From 29b5ab554790bb57337a3b6ab9dcd963cf69d22e Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 May 2024 14:47:08 +0200 Subject: Adding upstream version 1.7.2+ds. Signed-off-by: Daniel Baumann --- src/CMakeLists.txt | 214 ++ src/README.md | 12 + src/cli/CMakeLists.txt | 56 + src/cli/README.md | 26 + src/cli/cli.h | 20 + src/cli/cmd.c | 21 + src/cli/cmd.h | 33 + src/cli/cmd_cat_file.c | 204 ++ src/cli/cmd_clone.c | 192 ++ src/cli/cmd_hash_object.c | 154 ++ src/cli/cmd_help.c | 86 + src/cli/error.h | 51 + src/cli/main.c | 106 + src/cli/opt.c | 669 +++++ src/cli/opt.h | 349 +++ src/cli/opt_usage.c | 194 ++ src/cli/opt_usage.h | 35 + src/cli/progress.c | 346 +++ src/cli/progress.h | 117 + src/cli/sighandler.h | 20 + src/cli/unix/sighandler.c | 36 + src/cli/win32/precompiled.c | 1 + src/cli/win32/precompiled.h | 3 + src/cli/win32/sighandler.c | 37 + src/libgit2/CMakeLists.txt | 122 + src/libgit2/annotated_commit.c | 240 ++ src/libgit2/annotated_commit.h | 52 + src/libgit2/apply.c | 899 ++++++ src/libgit2/apply.h | 25 + src/libgit2/attr.c | 700 +++++ src/libgit2/attr.h | 15 + src/libgit2/attr_file.c | 1027 +++++++ src/libgit2/attr_file.h | 241 ++ src/libgit2/attrcache.c | 478 ++++ src/libgit2/attrcache.h | 56 + src/libgit2/blame.c | 566 ++++ src/libgit2/blame.h | 95 + src/libgit2/blame_git.c | 684 +++++ src/libgit2/blame_git.h | 22 + src/libgit2/blob.c | 530 ++++ src/libgit2/blob.h | 52 + src/libgit2/branch.c | 823 ++++++ src/libgit2/branch.h | 31 + src/libgit2/buf.c | 126 + src/libgit2/buf.h | 50 + src/libgit2/cache.c | 253 ++ src/libgit2/cache.h | 69 + src/libgit2/checkout.c | 2813 +++++++++++++++++++ src/libgit2/checkout.h | 27 + src/libgit2/cherrypick.c | 242 ++ src/libgit2/clone.c | 686 +++++ src/libgit2/clone.h | 20 + src/libgit2/commit.c | 1099 ++++++++ src/libgit2/commit.h | 87 + src/libgit2/commit_graph.c | 1309 +++++++++ src/libgit2/commit_graph.h | 188 ++ src/libgit2/commit_list.c | 219 ++ src/libgit2/commit_list.h | 58 + src/libgit2/common.h | 55 + src/libgit2/config.c | 1576 +++++++++++ src/libgit2/config.h | 110 + src/libgit2/config_backend.h | 96 + src/libgit2/config_cache.c | 142 + src/libgit2/config_entries.c | 237 ++ src/libgit2/config_entries.h | 24 + src/libgit2/config_file.c | 1200 ++++++++ src/libgit2/config_mem.c | 220 ++ src/libgit2/config_parse.c | 580 ++++ src/libgit2/config_parse.h | 65 + src/libgit2/config_snapshot.c | 207 ++ src/libgit2/crlf.c | 426 +++ src/libgit2/delta.c | 628 +++++ src/libgit2/delta.h | 136 + src/libgit2/describe.c | 912 ++++++ src/libgit2/diff.c | 402 +++ src/libgit2/diff.h | 67 + src/libgit2/diff_driver.c | 522 ++++ src/libgit2/diff_driver.h | 52 + src/libgit2/diff_file.c | 490 ++++ src/libgit2/diff_file.h | 63 + src/libgit2/diff_generate.c | 1750 ++++++++++++ src/libgit2/diff_generate.h | 130 + src/libgit2/diff_parse.c | 123 + src/libgit2/diff_parse.h | 20 + src/libgit2/diff_print.c | 849 ++++++ src/libgit2/diff_stats.c | 376 +++ src/libgit2/diff_stats.h | 18 + src/libgit2/diff_tform.c | 1125 ++++++++ src/libgit2/diff_tform.h | 25 + src/libgit2/diff_xdiff.c | 261 ++ src/libgit2/diff_xdiff.h | 35 + src/libgit2/email.c | 316 +++ src/libgit2/email.h | 25 + src/libgit2/errors.c | 293 ++ src/libgit2/errors.h | 80 + src/libgit2/experimental.h.in | 13 + src/libgit2/fetch.c | 239 ++ src/libgit2/fetch.h | 20 + src/libgit2/fetchhead.c | 340 +++ src/libgit2/fetchhead.h | 35 + src/libgit2/filter.c | 1221 ++++++++ src/libgit2/filter.h | 85 + src/libgit2/git2.rc | 59 + src/libgit2/grafts.c | 272 ++ src/libgit2/grafts.h | 36 + src/libgit2/graph.c | 249 ++ src/libgit2/hashsig.c | 375 +++ src/libgit2/ident.c | 139 + src/libgit2/idxmap.c | 157 ++ src/libgit2/idxmap.h | 177 ++ src/libgit2/ignore.c | 652 +++++ src/libgit2/ignore.h | 65 + src/libgit2/index.c | 3972 +++++++++++++++++++++++++++ src/libgit2/index.h | 207 ++ src/libgit2/indexer.c | 1483 ++++++++++ src/libgit2/indexer.h | 16 + src/libgit2/iterator.c | 2456 +++++++++++++++++ src/libgit2/iterator.h | 325 +++ src/libgit2/libgit2.c | 483 ++++ src/libgit2/libgit2.h | 15 + src/libgit2/mailmap.c | 500 ++++ src/libgit2/mailmap.h | 35 + src/libgit2/merge.c | 3440 +++++++++++++++++++++++ src/libgit2/merge.h | 204 ++ src/libgit2/merge_driver.c | 432 +++ src/libgit2/merge_driver.h | 62 + src/libgit2/merge_file.c | 325 +++ src/libgit2/message.c | 75 + src/libgit2/midx.c | 928 +++++++ src/libgit2/midx.h | 121 + src/libgit2/mwindow.c | 544 ++++ src/libgit2/mwindow.h | 57 + src/libgit2/notes.c | 810 ++++++ src/libgit2/notes.h | 32 + src/libgit2/object.c | 691 +++++ src/libgit2/object.h | 86 + src/libgit2/object_api.c | 148 + src/libgit2/odb.c | 2000 ++++++++++++++ src/libgit2/odb.h | 188 ++ src/libgit2/odb_loose.c | 1240 +++++++++ src/libgit2/odb_mempack.c | 189 ++ src/libgit2/odb_pack.c | 986 +++++++ src/libgit2/offmap.c | 101 + src/libgit2/offmap.h | 133 + src/libgit2/oid.c | 508 ++++ src/libgit2/oid.h | 273 ++ src/libgit2/oidarray.c | 89 + src/libgit2/oidarray.h | 24 + src/libgit2/oidmap.c | 107 + src/libgit2/oidmap.h | 128 + src/libgit2/pack-objects.c | 1839 +++++++++++++ src/libgit2/pack-objects.h | 109 + src/libgit2/pack.c | 1658 +++++++++++ src/libgit2/pack.h | 214 ++ src/libgit2/parse.c | 138 + src/libgit2/parse.h | 61 + src/libgit2/patch.c | 230 ++ src/libgit2/patch.h | 75 + src/libgit2/patch_generate.c | 934 +++++++ src/libgit2/patch_generate.h | 69 + src/libgit2/patch_parse.c | 1239 +++++++++ src/libgit2/patch_parse.h | 51 + src/libgit2/path.c | 375 +++ src/libgit2/path.h | 68 + src/libgit2/pathspec.c | 722 +++++ src/libgit2/pathspec.h | 76 + src/libgit2/proxy.c | 49 + src/libgit2/proxy.h | 17 + src/libgit2/push.c | 568 ++++ src/libgit2/push.h | 128 + src/libgit2/reader.c | 269 ++ src/libgit2/reader.h | 107 + src/libgit2/rebase.c | 1469 ++++++++++ src/libgit2/refdb.c | 424 +++ src/libgit2/refdb.h | 128 + src/libgit2/refdb_fs.c | 2508 +++++++++++++++++ src/libgit2/reflog.c | 234 ++ src/libgit2/reflog.h | 40 + src/libgit2/refs.c | 1404 ++++++++++ src/libgit2/refs.h | 130 + src/libgit2/refspec.c | 420 +++ src/libgit2/refspec.h | 54 + src/libgit2/remote.c | 3097 +++++++++++++++++++++ src/libgit2/remote.h | 101 + src/libgit2/repo_template.h | 58 + src/libgit2/repository.c | 3841 ++++++++++++++++++++++++++ src/libgit2/repository.h | 283 ++ src/libgit2/reset.c | 204 ++ src/libgit2/revert.c | 240 ++ src/libgit2/revparse.c | 968 +++++++ src/libgit2/revwalk.c | 846 ++++++ src/libgit2/revwalk.h | 73 + src/libgit2/settings.h | 11 + src/libgit2/signature.c | 339 +++ src/libgit2/signature.h | 23 + src/libgit2/stash.c | 1286 +++++++++ src/libgit2/status.c | 584 ++++ src/libgit2/status.h | 25 + src/libgit2/strarray.c | 65 + src/libgit2/strarray.h | 25 + src/libgit2/stream.h | 86 + src/libgit2/streams/mbedtls.c | 481 ++++ src/libgit2/streams/mbedtls.h | 23 + src/libgit2/streams/openssl.c | 739 +++++ src/libgit2/streams/openssl.h | 31 + src/libgit2/streams/openssl_dynamic.c | 313 +++ src/libgit2/streams/openssl_dynamic.h | 348 +++ src/libgit2/streams/openssl_legacy.c | 203 ++ src/libgit2/streams/openssl_legacy.h | 63 + src/libgit2/streams/registry.c | 119 + src/libgit2/streams/registry.h | 19 + src/libgit2/streams/schannel.c | 715 +++++ src/libgit2/streams/schannel.h | 28 + src/libgit2/streams/socket.c | 428 +++ src/libgit2/streams/socket.h | 25 + src/libgit2/streams/stransport.c | 354 +++ src/libgit2/streams/stransport.h | 21 + src/libgit2/streams/tls.c | 80 + src/libgit2/streams/tls.h | 31 + src/libgit2/submodule.c | 2384 ++++++++++++++++ src/libgit2/submodule.h | 164 ++ src/libgit2/sysdir.c | 650 +++++ src/libgit2/sysdir.h | 145 + src/libgit2/tag.c | 599 ++++ src/libgit2/tag.h | 31 + src/libgit2/threadstate.c | 97 + src/libgit2/threadstate.h | 22 + src/libgit2/trace.c | 25 + src/libgit2/trace.h | 51 + src/libgit2/trailer.c | 430 +++ src/libgit2/transaction.c | 395 +++ src/libgit2/transaction.h | 14 + src/libgit2/transport.c | 222 ++ src/libgit2/transports/auth.c | 74 + src/libgit2/transports/auth.h | 69 + src/libgit2/transports/auth_gssapi.c | 314 +++ src/libgit2/transports/auth_negotiate.h | 27 + src/libgit2/transports/auth_ntlm.h | 37 + src/libgit2/transports/auth_ntlmclient.c | 227 ++ src/libgit2/transports/auth_sspi.c | 341 +++ src/libgit2/transports/credential.c | 486 ++++ src/libgit2/transports/credential_helpers.c | 68 + src/libgit2/transports/git.c | 360 +++ src/libgit2/transports/http.c | 766 ++++++ src/libgit2/transports/http.h | 28 + src/libgit2/transports/httpclient.c | 1593 +++++++++++ src/libgit2/transports/httpclient.h | 200 ++ src/libgit2/transports/local.c | 777 ++++++ src/libgit2/transports/smart.c | 525 ++++ src/libgit2/transports/smart.h | 217 ++ src/libgit2/transports/smart_pkt.c | 870 ++++++ src/libgit2/transports/smart_protocol.c | 1231 +++++++++ src/libgit2/transports/ssh.c | 1147 ++++++++ src/libgit2/transports/ssh.h | 14 + src/libgit2/transports/winhttp.c | 1690 ++++++++++++ src/libgit2/tree-cache.c | 287 ++ src/libgit2/tree-cache.h | 40 + src/libgit2/tree.c | 1330 +++++++++ src/libgit2/tree.h | 58 + src/libgit2/userdiff.h | 210 ++ src/libgit2/worktree.c | 672 +++++ src/libgit2/worktree.h | 39 + src/util/CMakeLists.txt | 79 + src/util/alloc.c | 115 + src/util/alloc.h | 65 + src/util/allocators/failalloc.c | 32 + src/util/allocators/failalloc.h | 17 + src/util/allocators/stdalloc.c | 47 + src/util/allocators/stdalloc.h | 17 + src/util/allocators/win32_leakcheck.c | 50 + src/util/allocators/win32_leakcheck.h | 17 + src/util/array.h | 129 + src/util/assert_safe.h | 74 + src/util/bitvec.h | 75 + src/util/cc-compat.h | 108 + src/util/date.c | 899 ++++++ src/util/date.h | 33 + src/util/filebuf.c | 600 ++++ src/util/filebuf.h | 107 + src/util/fs_path.c | 2066 ++++++++++++++ src/util/fs_path.h | 790 ++++++ src/util/futils.c | 1236 +++++++++ src/util/futils.h | 403 +++ src/util/git2_features.h.in | 68 + src/util/git2_util.h | 168 ++ src/util/hash.c | 158 ++ src/util/hash.h | 61 + src/util/hash/builtin.c | 53 + src/util/hash/builtin.h | 19 + src/util/hash/collisiondetect.c | 48 + src/util/hash/collisiondetect.h | 19 + src/util/hash/common_crypto.c | 112 + src/util/hash/common_crypto.h | 27 + src/util/hash/mbedtls.c | 92 + src/util/hash/mbedtls.h | 29 + src/util/hash/openssl.c | 195 ++ src/util/hash/openssl.h | 45 + src/util/hash/rfc6234/sha.h | 243 ++ src/util/hash/rfc6234/sha224-256.c | 601 ++++ src/util/hash/sha.h | 70 + src/util/hash/sha1dc/sha1.c | 1909 +++++++++++++ src/util/hash/sha1dc/sha1.h | 110 + src/util/hash/sha1dc/ubc_check.c | 372 +++ src/util/hash/sha1dc/ubc_check.h | 52 + src/util/hash/win32.c | 549 ++++ src/util/hash/win32.h | 60 + src/util/integer.h | 218 ++ src/util/khash.h | 615 +++++ src/util/map.h | 46 + src/util/net.c | 1154 ++++++++ src/util/net.h | 110 + src/util/pool.c | 260 ++ src/util/pool.h | 146 + src/util/posix.c | 357 +++ src/util/posix.h | 220 ++ src/util/pqueue.c | 125 + src/util/pqueue.h | 59 + src/util/rand.c | 236 ++ src/util/rand.h | 37 + src/util/regexp.c | 221 ++ src/util/regexp.h | 97 + src/util/runtime.c | 162 ++ src/util/runtime.h | 62 + src/util/sortedcache.c | 380 +++ src/util/sortedcache.h | 182 ++ src/util/staticstr.h | 66 + src/util/str.c | 1372 +++++++++ src/util/str.h | 357 +++ src/util/strmap.c | 100 + src/util/strmap.h | 131 + src/util/strnlen.h | 24 + src/util/thread.c | 140 + src/util/thread.h | 480 ++++ src/util/tsort.c | 382 +++ src/util/unix/map.c | 76 + src/util/unix/posix.h | 104 + src/util/unix/pthread.h | 57 + src/util/unix/realpath.c | 32 + src/util/utf8.c | 150 + src/util/utf8.h | 52 + src/util/util.c | 824 ++++++ src/util/util.h | 396 +++ src/util/varint.c | 43 + src/util/varint.h | 17 + src/util/vector.c | 431 +++ src/util/vector.h | 128 + src/util/wildmatch.c | 320 +++ src/util/wildmatch.h | 23 + src/util/win32/dir.c | 122 + src/util/win32/dir.h | 44 + src/util/win32/error.c | 53 + src/util/win32/error.h | 15 + src/util/win32/map.c | 141 + src/util/win32/mingw-compat.h | 23 + src/util/win32/msvc-compat.h | 36 + src/util/win32/path_w32.c | 642 +++++ src/util/win32/path_w32.h | 91 + src/util/win32/posix.h | 62 + src/util/win32/posix_w32.c | 1047 +++++++ src/util/win32/precompiled.c | 1 + src/util/win32/precompiled.h | 21 + src/util/win32/reparse.h | 57 + src/util/win32/thread.c | 262 ++ src/util/win32/thread.h | 64 + src/util/win32/utf-conv.c | 144 + src/util/win32/utf-conv.h | 127 + src/util/win32/version.h | 37 + src/util/win32/w32_buffer.c | 57 + src/util/win32/w32_buffer.h | 19 + src/util/win32/w32_common.h | 48 + src/util/win32/w32_leakcheck.c | 581 ++++ src/util/win32/w32_leakcheck.h | 222 ++ src/util/win32/w32_util.c | 126 + src/util/win32/w32_util.h | 144 + src/util/win32/win32-compat.h | 52 + src/util/zstream.c | 210 ++ src/util/zstream.h | 54 + 377 files changed, 135891 insertions(+) create mode 100644 src/CMakeLists.txt create mode 100644 src/README.md create mode 100644 src/cli/CMakeLists.txt create mode 100644 src/cli/README.md create mode 100644 src/cli/cli.h create mode 100644 src/cli/cmd.c create mode 100644 src/cli/cmd.h create mode 100644 src/cli/cmd_cat_file.c create mode 100644 src/cli/cmd_clone.c create mode 100644 src/cli/cmd_hash_object.c create mode 100644 src/cli/cmd_help.c create mode 100644 src/cli/error.h create mode 100644 src/cli/main.c create mode 100644 src/cli/opt.c create mode 100644 src/cli/opt.h create mode 100644 src/cli/opt_usage.c create mode 100644 src/cli/opt_usage.h create mode 100644 src/cli/progress.c create mode 100644 src/cli/progress.h create mode 100644 src/cli/sighandler.h create mode 100644 src/cli/unix/sighandler.c create mode 100644 src/cli/win32/precompiled.c create mode 100644 src/cli/win32/precompiled.h create mode 100644 src/cli/win32/sighandler.c create mode 100644 src/libgit2/CMakeLists.txt create mode 100644 src/libgit2/annotated_commit.c create mode 100644 src/libgit2/annotated_commit.h create mode 100644 src/libgit2/apply.c create mode 100644 src/libgit2/apply.h create mode 100644 src/libgit2/attr.c create mode 100644 src/libgit2/attr.h create mode 100644 src/libgit2/attr_file.c create mode 100644 src/libgit2/attr_file.h create mode 100644 src/libgit2/attrcache.c create mode 100644 src/libgit2/attrcache.h create mode 100644 src/libgit2/blame.c create mode 100644 src/libgit2/blame.h create mode 100644 src/libgit2/blame_git.c create mode 100644 src/libgit2/blame_git.h create mode 100644 src/libgit2/blob.c create mode 100644 src/libgit2/blob.h create mode 100644 src/libgit2/branch.c create mode 100644 src/libgit2/branch.h create mode 100644 src/libgit2/buf.c create mode 100644 src/libgit2/buf.h create mode 100644 src/libgit2/cache.c create mode 100644 src/libgit2/cache.h create mode 100644 src/libgit2/checkout.c create mode 100644 src/libgit2/checkout.h create mode 100644 src/libgit2/cherrypick.c create mode 100644 src/libgit2/clone.c create mode 100644 src/libgit2/clone.h create mode 100644 src/libgit2/commit.c create mode 100644 src/libgit2/commit.h create mode 100644 src/libgit2/commit_graph.c create mode 100644 src/libgit2/commit_graph.h create mode 100644 src/libgit2/commit_list.c create mode 100644 src/libgit2/commit_list.h create mode 100644 src/libgit2/common.h create mode 100644 src/libgit2/config.c create mode 100644 src/libgit2/config.h create mode 100644 src/libgit2/config_backend.h create mode 100644 src/libgit2/config_cache.c create mode 100644 src/libgit2/config_entries.c create mode 100644 src/libgit2/config_entries.h create mode 100644 src/libgit2/config_file.c create mode 100644 src/libgit2/config_mem.c create mode 100644 src/libgit2/config_parse.c create mode 100644 src/libgit2/config_parse.h create mode 100644 src/libgit2/config_snapshot.c create mode 100644 src/libgit2/crlf.c create mode 100644 src/libgit2/delta.c create mode 100644 src/libgit2/delta.h create mode 100644 src/libgit2/describe.c create mode 100644 src/libgit2/diff.c create mode 100644 src/libgit2/diff.h create mode 100644 src/libgit2/diff_driver.c create mode 100644 src/libgit2/diff_driver.h create mode 100644 src/libgit2/diff_file.c create mode 100644 src/libgit2/diff_file.h create mode 100644 src/libgit2/diff_generate.c create mode 100644 src/libgit2/diff_generate.h create mode 100644 src/libgit2/diff_parse.c create mode 100644 src/libgit2/diff_parse.h create mode 100644 src/libgit2/diff_print.c create mode 100644 src/libgit2/diff_stats.c create mode 100644 src/libgit2/diff_stats.h create mode 100644 src/libgit2/diff_tform.c create mode 100644 src/libgit2/diff_tform.h create mode 100644 src/libgit2/diff_xdiff.c create mode 100644 src/libgit2/diff_xdiff.h create mode 100644 src/libgit2/email.c create mode 100644 src/libgit2/email.h create mode 100644 src/libgit2/errors.c create mode 100644 src/libgit2/errors.h create mode 100644 src/libgit2/experimental.h.in create mode 100644 src/libgit2/fetch.c create mode 100644 src/libgit2/fetch.h create mode 100644 src/libgit2/fetchhead.c create mode 100644 src/libgit2/fetchhead.h create mode 100644 src/libgit2/filter.c create mode 100644 src/libgit2/filter.h create mode 100644 src/libgit2/git2.rc create mode 100644 src/libgit2/grafts.c create mode 100644 src/libgit2/grafts.h create mode 100644 src/libgit2/graph.c create mode 100644 src/libgit2/hashsig.c create mode 100644 src/libgit2/ident.c create mode 100644 src/libgit2/idxmap.c create mode 100644 src/libgit2/idxmap.h create mode 100644 src/libgit2/ignore.c create mode 100644 src/libgit2/ignore.h create mode 100644 src/libgit2/index.c create mode 100644 src/libgit2/index.h create mode 100644 src/libgit2/indexer.c create mode 100644 src/libgit2/indexer.h create mode 100644 src/libgit2/iterator.c create mode 100644 src/libgit2/iterator.h create mode 100644 src/libgit2/libgit2.c create mode 100644 src/libgit2/libgit2.h create mode 100644 src/libgit2/mailmap.c create mode 100644 src/libgit2/mailmap.h create mode 100644 src/libgit2/merge.c create mode 100644 src/libgit2/merge.h create mode 100644 src/libgit2/merge_driver.c create mode 100644 src/libgit2/merge_driver.h create mode 100644 src/libgit2/merge_file.c create mode 100644 src/libgit2/message.c create mode 100644 src/libgit2/midx.c create mode 100644 src/libgit2/midx.h create mode 100644 src/libgit2/mwindow.c create mode 100644 src/libgit2/mwindow.h create mode 100644 src/libgit2/notes.c create mode 100644 src/libgit2/notes.h create mode 100644 src/libgit2/object.c create mode 100644 src/libgit2/object.h create mode 100644 src/libgit2/object_api.c create mode 100644 src/libgit2/odb.c create mode 100644 src/libgit2/odb.h create mode 100644 src/libgit2/odb_loose.c create mode 100644 src/libgit2/odb_mempack.c create mode 100644 src/libgit2/odb_pack.c create mode 100644 src/libgit2/offmap.c create mode 100644 src/libgit2/offmap.h create mode 100644 src/libgit2/oid.c create mode 100644 src/libgit2/oid.h create mode 100644 src/libgit2/oidarray.c create mode 100644 src/libgit2/oidarray.h create mode 100644 src/libgit2/oidmap.c create mode 100644 src/libgit2/oidmap.h create mode 100644 src/libgit2/pack-objects.c create mode 100644 src/libgit2/pack-objects.h create mode 100644 src/libgit2/pack.c create mode 100644 src/libgit2/pack.h create mode 100644 src/libgit2/parse.c create mode 100644 src/libgit2/parse.h create mode 100644 src/libgit2/patch.c create mode 100644 src/libgit2/patch.h create mode 100644 src/libgit2/patch_generate.c create mode 100644 src/libgit2/patch_generate.h create mode 100644 src/libgit2/patch_parse.c create mode 100644 src/libgit2/patch_parse.h create mode 100644 src/libgit2/path.c create mode 100644 src/libgit2/path.h create mode 100644 src/libgit2/pathspec.c create mode 100644 src/libgit2/pathspec.h create mode 100644 src/libgit2/proxy.c create mode 100644 src/libgit2/proxy.h create mode 100644 src/libgit2/push.c create mode 100644 src/libgit2/push.h create mode 100644 src/libgit2/reader.c create mode 100644 src/libgit2/reader.h create mode 100644 src/libgit2/rebase.c create mode 100644 src/libgit2/refdb.c create mode 100644 src/libgit2/refdb.h create mode 100644 src/libgit2/refdb_fs.c create mode 100644 src/libgit2/reflog.c create mode 100644 src/libgit2/reflog.h create mode 100644 src/libgit2/refs.c create mode 100644 src/libgit2/refs.h create mode 100644 src/libgit2/refspec.c create mode 100644 src/libgit2/refspec.h create mode 100644 src/libgit2/remote.c create mode 100644 src/libgit2/remote.h create mode 100644 src/libgit2/repo_template.h create mode 100644 src/libgit2/repository.c create mode 100644 src/libgit2/repository.h create mode 100644 src/libgit2/reset.c create mode 100644 src/libgit2/revert.c create mode 100644 src/libgit2/revparse.c create mode 100644 src/libgit2/revwalk.c create mode 100644 src/libgit2/revwalk.h create mode 100644 src/libgit2/settings.h create mode 100644 src/libgit2/signature.c create mode 100644 src/libgit2/signature.h create mode 100644 src/libgit2/stash.c create mode 100644 src/libgit2/status.c create mode 100644 src/libgit2/status.h create mode 100644 src/libgit2/strarray.c create mode 100644 src/libgit2/strarray.h create mode 100644 src/libgit2/stream.h create mode 100644 src/libgit2/streams/mbedtls.c create mode 100644 src/libgit2/streams/mbedtls.h create mode 100644 src/libgit2/streams/openssl.c create mode 100644 src/libgit2/streams/openssl.h create mode 100644 src/libgit2/streams/openssl_dynamic.c create mode 100644 src/libgit2/streams/openssl_dynamic.h create mode 100644 src/libgit2/streams/openssl_legacy.c create mode 100644 src/libgit2/streams/openssl_legacy.h create mode 100644 src/libgit2/streams/registry.c create mode 100644 src/libgit2/streams/registry.h create mode 100644 src/libgit2/streams/schannel.c create mode 100644 src/libgit2/streams/schannel.h create mode 100644 src/libgit2/streams/socket.c create mode 100644 src/libgit2/streams/socket.h create mode 100644 src/libgit2/streams/stransport.c create mode 100644 src/libgit2/streams/stransport.h create mode 100644 src/libgit2/streams/tls.c create mode 100644 src/libgit2/streams/tls.h create mode 100644 src/libgit2/submodule.c create mode 100644 src/libgit2/submodule.h create mode 100644 src/libgit2/sysdir.c create mode 100644 src/libgit2/sysdir.h create mode 100644 src/libgit2/tag.c create mode 100644 src/libgit2/tag.h create mode 100644 src/libgit2/threadstate.c create mode 100644 src/libgit2/threadstate.h create mode 100644 src/libgit2/trace.c create mode 100644 src/libgit2/trace.h create mode 100644 src/libgit2/trailer.c create mode 100644 src/libgit2/transaction.c create mode 100644 src/libgit2/transaction.h create mode 100644 src/libgit2/transport.c create mode 100644 src/libgit2/transports/auth.c create mode 100644 src/libgit2/transports/auth.h create mode 100644 src/libgit2/transports/auth_gssapi.c create mode 100644 src/libgit2/transports/auth_negotiate.h create mode 100644 src/libgit2/transports/auth_ntlm.h create mode 100644 src/libgit2/transports/auth_ntlmclient.c create mode 100644 src/libgit2/transports/auth_sspi.c create mode 100644 src/libgit2/transports/credential.c create mode 100644 src/libgit2/transports/credential_helpers.c create mode 100644 src/libgit2/transports/git.c create mode 100644 src/libgit2/transports/http.c create mode 100644 src/libgit2/transports/http.h create mode 100644 src/libgit2/transports/httpclient.c create mode 100644 src/libgit2/transports/httpclient.h create mode 100644 src/libgit2/transports/local.c create mode 100644 src/libgit2/transports/smart.c create mode 100644 src/libgit2/transports/smart.h create mode 100644 src/libgit2/transports/smart_pkt.c create mode 100644 src/libgit2/transports/smart_protocol.c create mode 100644 src/libgit2/transports/ssh.c create mode 100644 src/libgit2/transports/ssh.h create mode 100644 src/libgit2/transports/winhttp.c create mode 100644 src/libgit2/tree-cache.c create mode 100644 src/libgit2/tree-cache.h create mode 100644 src/libgit2/tree.c create mode 100644 src/libgit2/tree.h create mode 100644 src/libgit2/userdiff.h create mode 100644 src/libgit2/worktree.c create mode 100644 src/libgit2/worktree.h create mode 100644 src/util/CMakeLists.txt create mode 100644 src/util/alloc.c create mode 100644 src/util/alloc.h create mode 100644 src/util/allocators/failalloc.c create mode 100644 src/util/allocators/failalloc.h create mode 100644 src/util/allocators/stdalloc.c create mode 100644 src/util/allocators/stdalloc.h create mode 100644 src/util/allocators/win32_leakcheck.c create mode 100644 src/util/allocators/win32_leakcheck.h create mode 100644 src/util/array.h create mode 100644 src/util/assert_safe.h create mode 100644 src/util/bitvec.h create mode 100644 src/util/cc-compat.h create mode 100644 src/util/date.c create mode 100644 src/util/date.h create mode 100644 src/util/filebuf.c create mode 100644 src/util/filebuf.h create mode 100644 src/util/fs_path.c create mode 100644 src/util/fs_path.h create mode 100644 src/util/futils.c create mode 100644 src/util/futils.h create mode 100644 src/util/git2_features.h.in create mode 100644 src/util/git2_util.h create mode 100644 src/util/hash.c create mode 100644 src/util/hash.h create mode 100644 src/util/hash/builtin.c create mode 100644 src/util/hash/builtin.h create mode 100644 src/util/hash/collisiondetect.c create mode 100644 src/util/hash/collisiondetect.h create mode 100644 src/util/hash/common_crypto.c create mode 100644 src/util/hash/common_crypto.h create mode 100644 src/util/hash/mbedtls.c create mode 100644 src/util/hash/mbedtls.h create mode 100644 src/util/hash/openssl.c create mode 100644 src/util/hash/openssl.h create mode 100644 src/util/hash/rfc6234/sha.h create mode 100644 src/util/hash/rfc6234/sha224-256.c create mode 100644 src/util/hash/sha.h create mode 100644 src/util/hash/sha1dc/sha1.c create mode 100644 src/util/hash/sha1dc/sha1.h create mode 100644 src/util/hash/sha1dc/ubc_check.c create mode 100644 src/util/hash/sha1dc/ubc_check.h create mode 100644 src/util/hash/win32.c create mode 100644 src/util/hash/win32.h create mode 100644 src/util/integer.h create mode 100644 src/util/khash.h create mode 100644 src/util/map.h create mode 100644 src/util/net.c create mode 100644 src/util/net.h create mode 100644 src/util/pool.c create mode 100644 src/util/pool.h create mode 100644 src/util/posix.c create mode 100644 src/util/posix.h create mode 100644 src/util/pqueue.c create mode 100644 src/util/pqueue.h create mode 100644 src/util/rand.c create mode 100644 src/util/rand.h create mode 100644 src/util/regexp.c create mode 100644 src/util/regexp.h create mode 100644 src/util/runtime.c create mode 100644 src/util/runtime.h create mode 100644 src/util/sortedcache.c create mode 100644 src/util/sortedcache.h create mode 100644 src/util/staticstr.h create mode 100644 src/util/str.c create mode 100644 src/util/str.h create mode 100644 src/util/strmap.c create mode 100644 src/util/strmap.h create mode 100644 src/util/strnlen.h create mode 100644 src/util/thread.c create mode 100644 src/util/thread.h create mode 100644 src/util/tsort.c create mode 100644 src/util/unix/map.c create mode 100644 src/util/unix/posix.h create mode 100644 src/util/unix/pthread.h create mode 100644 src/util/unix/realpath.c create mode 100644 src/util/utf8.c create mode 100644 src/util/utf8.h create mode 100644 src/util/util.c create mode 100644 src/util/util.h create mode 100644 src/util/varint.c create mode 100644 src/util/varint.h create mode 100644 src/util/vector.c create mode 100644 src/util/vector.h create mode 100644 src/util/wildmatch.c create mode 100644 src/util/wildmatch.h create mode 100644 src/util/win32/dir.c create mode 100644 src/util/win32/dir.h create mode 100644 src/util/win32/error.c create mode 100644 src/util/win32/error.h create mode 100644 src/util/win32/map.c create mode 100644 src/util/win32/mingw-compat.h create mode 100644 src/util/win32/msvc-compat.h create mode 100644 src/util/win32/path_w32.c create mode 100644 src/util/win32/path_w32.h create mode 100644 src/util/win32/posix.h create mode 100644 src/util/win32/posix_w32.c create mode 100644 src/util/win32/precompiled.c create mode 100644 src/util/win32/precompiled.h create mode 100644 src/util/win32/reparse.h create mode 100644 src/util/win32/thread.c create mode 100644 src/util/win32/thread.h create mode 100644 src/util/win32/utf-conv.c create mode 100644 src/util/win32/utf-conv.h create mode 100644 src/util/win32/version.h create mode 100644 src/util/win32/w32_buffer.c create mode 100644 src/util/win32/w32_buffer.h create mode 100644 src/util/win32/w32_common.h create mode 100644 src/util/win32/w32_leakcheck.c create mode 100644 src/util/win32/w32_leakcheck.h create mode 100644 src/util/win32/w32_util.c create mode 100644 src/util/win32/w32_util.h create mode 100644 src/util/win32/win32-compat.h create mode 100644 src/util/zstream.c create mode 100644 src/util/zstream.h (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..8525acd --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,214 @@ +# The main libgit2 source tree: this CMakeLists.txt identifies platform +# support and includes the subprojects that make up core libgit2 support. + +# +# Optional build configuration settings +# + +if(DEPRECATE_HARD) + add_definitions(-DGIT_DEPRECATE_HARD) +endif() + +if(USE_LEAK_CHECKER STREQUAL "valgrind") + add_definitions(-DVALGRIND) +endif() + +# +# Optional debugging functionality +# + +if(DEBUG_POOL) + set(GIT_DEBUG_POOL 1) +endif() +add_feature_info(debugpool GIT_DEBUG_POOL "debug pool allocator") + +if(DEBUG_STRICT_ALLOC) + set(GIT_DEBUG_STRICT_ALLOC 1) +endif() +add_feature_info(debugalloc GIT_DEBUG_STRICT_ALLOC "debug strict allocators") + +if(DEBUG_STRICT_OPEN) + set(GIT_DEBUG_STRICT_OPEN 1) +endif() +add_feature_info(debugopen GIT_DEBUG_STRICT_OPEN "path validation in open") + +# +# Optional feature enablement +# + +include(SelectGSSAPI) +include(SelectHTTPSBackend) +include(SelectHashes) +include(SelectHTTPParser) +include(SelectRegex) +include(SelectXdiff) +include(SelectSSH) +include(SelectZlib) + +# +# Platform support +# + +# futimes/futimens + +if(HAVE_FUTIMENS) + set(GIT_USE_FUTIMENS 1) +endif () +add_feature_info(futimens GIT_USE_FUTIMENS "futimens support") + +# qsort + +# old-style FreeBSD qsort_r() has the 'context' parameter as the first argument +# of the comparison function: +check_prototype_definition_safe(qsort_r + "void (qsort_r)(void *base, size_t nmemb, size_t size, void *context, int (*compar)(void *, const void *, const void *))" + "" "stdlib.h" GIT_QSORT_BSD) + +# GNU or POSIX qsort_r() has the 'context' parameter as the last argument of the +# comparison function: +check_prototype_definition_safe(qsort_r + "void (qsort_r)(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *, void *), void *context)" + "" "stdlib.h" GIT_QSORT_GNU) + +# C11 qsort_s() has the 'context' parameter as the last argument of the +# comparison function, and returns an error status: +check_prototype_definition_safe(qsort_s + "errno_t (qsort_s)(void *base, rsize_t nmemb, rsize_t size, int (*compar)(const void *, const void *, void *), void *context)" + "0" "stdlib.h" GIT_QSORT_C11) + +# MSC qsort_s() has the 'context' parameter as the first argument of the +# comparison function, and as the last argument of qsort_s(): +check_prototype_definition_safe(qsort_s + "void (qsort_s)(void *base, size_t num, size_t width, int (*compare )(void *, const void *, const void *), void *context)" + "" "stdlib.h" GIT_QSORT_MSC) + +# random / entropy data + +check_symbol_exists(getentropy unistd.h GIT_RAND_GETENTROPY) +check_symbol_exists(getloadavg stdlib.h GIT_RAND_GETLOADAVG) + +# poll + +if(WIN32) + set(GIT_IO_WSAPOLL 1) +else() + check_symbol_exists(poll poll.h GIT_IO_POLL) + check_symbol_exists(select sys/select.h GIT_IO_SELECT) +endif() + +# determine architecture of the machine + +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(GIT_ARCH_64 1) +elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) + set(GIT_ARCH_32 1) +elseif(CMAKE_SIZEOF_VOID_P) + message(FATAL_ERROR "Unsupported architecture (pointer size is ${CMAKE_SIZEOF_VOID_P} bytes)") +else() + message(FATAL_ERROR "Unsupported architecture (CMAKE_SIZEOF_VOID_P is unset)") +endif() + +# nanosecond mtime/ctime support + +if(USE_NSEC) + set(GIT_USE_NSEC 1) +endif() + +# high-resolution stat support + +if(HAVE_STRUCT_STAT_ST_MTIM) + set(GIT_USE_STAT_MTIM 1) +elseif(HAVE_STRUCT_STAT_ST_MTIMESPEC) + set(GIT_USE_STAT_MTIMESPEC 1) +elseif(HAVE_STRUCT_STAT_ST_MTIME_NSEC) + set(GIT_USE_STAT_MTIME_NSEC 1) +endif() + +# realtime support + +check_library_exists(rt clock_gettime "time.h" NEED_LIBRT) +if(NEED_LIBRT) + list(APPEND LIBGIT2_SYSTEM_LIBS rt) + list(APPEND LIBGIT2_PC_LIBS "-lrt") +endif() + +# platform libraries + +if(WIN32) + list(APPEND LIBGIT2_SYSTEM_LIBS ws2_32) +endif() + +if(CMAKE_SYSTEM_NAME MATCHES "(Solaris|SunOS)") + list(APPEND LIBGIT2_SYSTEM_LIBS socket nsl) + list(APPEND LIBGIT2_PC_LIBS "-lsocket" "-lnsl") +endif() + +if(CMAKE_SYSTEM_NAME MATCHES "Haiku") + list(APPEND LIBGIT2_SYSTEM_LIBS gnu network) + list(APPEND LIBGIT2_PC_LIBS "-lgnu -lnetwork") +endif() + +if(AMIGA) + add_definitions(-DNO_ADDRINFO -DNO_READDIR_R -DNO_MMAP) +endif() + +# threads + +if(USE_THREADS) + if(NOT WIN32) + find_package(Threads REQUIRED) + list(APPEND LIBGIT2_SYSTEM_LIBS ${CMAKE_THREAD_LIBS_INIT}) + list(APPEND LIBGIT2_PC_LIBS ${CMAKE_THREAD_LIBS_INIT}) + endif() + + set(GIT_THREADS 1) +endif() +add_feature_info(threadsafe USE_THREADS "threadsafe support") + +# +# Optional bundled features +# + +# ntlmclient +if(USE_NTLMCLIENT) + set(GIT_NTLM 1) + add_subdirectory("${PROJECT_SOURCE_DIR}/deps/ntlmclient" "${PROJECT_BINARY_DIR}/deps/ntlmclient") + list(APPEND LIBGIT2_DEPENDENCY_INCLUDES "${PROJECT_SOURCE_DIR}/deps/ntlmclient") + list(APPEND LIBGIT2_DEPENDENCY_OBJECTS "$") +endif() +add_feature_info(ntlmclient GIT_NTLM "NTLM authentication support for Unix") + +# +# Optional external dependencies + +# iconv +if(USE_ICONV) + find_package(Iconv) +endif() +if(ICONV_FOUND) + set(GIT_USE_ICONV 1) + list(APPEND LIBGIT2_SYSTEM_INCLUDES ${ICONV_INCLUDE_DIR}) + list(APPEND LIBGIT2_SYSTEM_LIBS ${ICONV_LIBRARIES}) + list(APPEND LIBGIT2_PC_LIBS ${ICONV_LIBRARIES}) +endif() +add_feature_info(iconv GIT_USE_ICONV "iconv encoding conversion support") + +# +# Include child projects +# + +add_subdirectory(libgit2) +add_subdirectory(util) + +if(BUILD_CLI) + add_subdirectory(cli) +endif() + +# re-export these to the root so that peer projects (tests, fuzzers, +# examples) can use them +set(LIBGIT2_INCLUDES ${LIBGIT2_INCLUDES} PARENT_SCOPE) +set(LIBGIT2_OBJECTS ${LIBGIT2_OBJECTS} PARENT_SCOPE) +set(LIBGIT2_DEPENDENCY_INCLUDES ${LIBGIT2_DEPENDENCY_INCLUDES} PARENT_SCOPE) +set(LIBGIT2_DEPENDENCY_OBJECTS ${LIBGIT2_DEPENDENCY_OBJECTS} PARENT_SCOPE) +set(LIBGIT2_SYSTEM_INCLUDES ${LIBGIT2_SYSTEM_INCLUDES} PARENT_SCOPE) +set(LIBGIT2_SYSTEM_LIBS ${LIBGIT2_SYSTEM_LIBS} PARENT_SCOPE) diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..10b86c1 --- /dev/null +++ b/src/README.md @@ -0,0 +1,12 @@ +# libgit2 sources + +This is the source that makes up the core of libgit2 and its related +projects. + +* `cli` + A git-compatible command-line interface that uses libgit2. +* `libgit2` + This is the libgit2 project, a cross-platform, linkable library + implementation of Git that you can use in your application. +* `util` + A shared utility library for these projects. diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt new file mode 100644 index 0000000..84b6c19 --- /dev/null +++ b/src/cli/CMakeLists.txt @@ -0,0 +1,56 @@ +set(CLI_INCLUDES + "${libgit2_BINARY_DIR}/src/util" + "${libgit2_BINARY_DIR}/include" + "${libgit2_SOURCE_DIR}/src/util" + "${libgit2_SOURCE_DIR}/src/cli" + "${libgit2_SOURCE_DIR}/include" + "${LIBGIT2_DEPENDENCY_INCLUDES}") + +if(WIN32 AND NOT CYGWIN) + file(GLOB CLI_SRC_OS win32/*.c) + list(SORT CLI_SRC_OS) +else() + file(GLOB CLI_SRC_OS unix/*.c) + list(SORT CLI_SRC_OS) +endif() + +file(GLOB CLI_SRC_C *.c *.h) +list(SORT CLI_SRC_C) + +# +# The CLI currently needs to be statically linked against libgit2 because +# the utility library uses libgit2's thread-local error buffers. TODO: +# remove this dependency and allow us to dynamically link against libgit2. +# + +if(BUILD_CLI STREQUAL "dynamic") + set(CLI_LIBGIT2_LIBRARY libgit2package) +else() + set(CLI_LIBGIT2_OBJECTS $) +endif() + +# +# Compile and link the CLI +# + +add_executable(git2_cli ${CLI_SRC_C} ${CLI_SRC_OS} ${CLI_OBJECTS} + $ + ${CLI_LIBGIT2_OBJECTS} + ${LIBGIT2_DEPENDENCY_OBJECTS}) +target_link_libraries(git2_cli ${CLI_LIBGIT2_LIBRARY} ${LIBGIT2_SYSTEM_LIBS}) + +set_target_properties(git2_cli PROPERTIES C_STANDARD 90) +set_target_properties(git2_cli PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${libgit2_BINARY_DIR}) +set_target_properties(git2_cli PROPERTIES OUTPUT_NAME ${LIBGIT2_FILENAME}) + +ide_split_sources(git2_cli) + +target_include_directories(git2_cli PRIVATE ${CLI_INCLUDES}) + +if(MSVC_IDE) + # Precompiled headers + set_target_properties(git2_cli PROPERTIES COMPILE_FLAGS "/Yuprecompiled.h /FIprecompiled.h") + set_source_files_properties(win32/precompiled.c COMPILE_FLAGS "/Ycprecompiled.h") +endif() + +install(TARGETS git2_cli RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/cli/README.md b/src/cli/README.md new file mode 100644 index 0000000..3087c39 --- /dev/null +++ b/src/cli/README.md @@ -0,0 +1,26 @@ +# cli + +A git-compatible command-line interface that uses libgit2. + +## Adding commands + +1. Individual commands have a `main`-like top-level entrypoint. For example: + + ```c + int cmd_help(int argc, char **argv) + ``` + + Although this is the same signature as `main`, commands are not built as + individual standalone executables, they'll be linked into the main cli. + (Though there may be an option for command executables to be built as + standalone executables in the future.) + +2. Commands are prototyped in `cmd.h` and added to `main.c`'s list of + commands (`cli_cmds[]`). Commands should be specified with their name, + entrypoint and a brief description that can be printed in `git help`. + This is done because commands are linked into the main cli. + +3. Commands should accept a `--help` option that displays their help + information. This will be shown when a user runs ` --help` and + when a user runs `help `. + diff --git a/src/cli/cli.h b/src/cli/cli.h new file mode 100644 index 0000000..7dede67 --- /dev/null +++ b/src/cli/cli.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef CLI_cli_h__ +#define CLI_cli_h__ + +#define PROGRAM_NAME "git2" + +#include "git2_util.h" + +#include "error.h" +#include "opt.h" +#include "opt_usage.h" +#include "sighandler.h" + +#endif /* CLI_cli_h__ */ diff --git a/src/cli/cmd.c b/src/cli/cmd.c new file mode 100644 index 0000000..2a7e71c --- /dev/null +++ b/src/cli/cmd.c @@ -0,0 +1,21 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "cli.h" +#include "cmd.h" + +const cli_cmd_spec *cli_cmd_spec_byname(const char *name) +{ + const cli_cmd_spec *cmd; + + for (cmd = cli_cmds; cmd->name; cmd++) { + if (!strcmp(cmd->name, name)) + return cmd; + } + + return NULL; +} diff --git a/src/cli/cmd.h b/src/cli/cmd.h new file mode 100644 index 0000000..8b1a1b3 --- /dev/null +++ b/src/cli/cmd.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef CLI_cmd_h__ +#define CLI_cmd_h__ + +/* Command definitions */ +typedef struct { + const char *name; + int (*fn)(int argc, char **argv); + const char *desc; +} cli_cmd_spec; + +/* Options that are common to all commands (eg --help, --git-dir) */ +extern const cli_opt_spec cli_common_opts[]; + +/* All the commands supported by the CLI */ +extern const cli_cmd_spec cli_cmds[]; + +/* Find a command by name */ +extern const cli_cmd_spec *cli_cmd_spec_byname(const char *name); + +/* Commands */ +extern int cmd_cat_file(int argc, char **argv); +extern int cmd_clone(int argc, char **argv); +extern int cmd_hash_object(int argc, char **argv); +extern int cmd_help(int argc, char **argv); + +#endif /* CLI_cmd_h__ */ diff --git a/src/cli/cmd_cat_file.c b/src/cli/cmd_cat_file.c new file mode 100644 index 0000000..fb53a72 --- /dev/null +++ b/src/cli/cmd_cat_file.c @@ -0,0 +1,204 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include +#include "cli.h" +#include "cmd.h" + +#define COMMAND_NAME "cat-file" + +typedef enum { + DISPLAY_CONTENT = 0, + DISPLAY_EXISTS, + DISPLAY_PRETTY, + DISPLAY_SIZE, + DISPLAY_TYPE +} display_t; + +static int show_help; +static int display = DISPLAY_CONTENT; +static char *type_name, *object_spec; + +static const cli_opt_spec opts[] = { + { CLI_OPT_TYPE_SWITCH, "help", 0, &show_help, 1, + CLI_OPT_USAGE_HIDDEN | CLI_OPT_USAGE_STOP_PARSING, NULL, + "display help about the " COMMAND_NAME " command" }, + + { CLI_OPT_TYPE_SWITCH, NULL, 't', &display, DISPLAY_TYPE, + CLI_OPT_USAGE_REQUIRED, NULL, "display the type of the object" }, + { CLI_OPT_TYPE_SWITCH, NULL, 's', &display, DISPLAY_SIZE, + CLI_OPT_USAGE_CHOICE, NULL, "display the size of the object" }, + { CLI_OPT_TYPE_SWITCH, NULL, 'e', &display, DISPLAY_EXISTS, + CLI_OPT_USAGE_CHOICE, NULL, "displays nothing unless the object is corrupt" }, + { CLI_OPT_TYPE_SWITCH, NULL, 'p', &display, DISPLAY_PRETTY, + CLI_OPT_USAGE_CHOICE, NULL, "pretty-print the object" }, + { CLI_OPT_TYPE_ARG, "type", 0, &type_name, 0, + CLI_OPT_USAGE_CHOICE, "type", "the type of object to display" }, + { CLI_OPT_TYPE_ARG, "object", 0, &object_spec, 0, + CLI_OPT_USAGE_REQUIRED, "object", "the object to display" }, + { 0 }, +}; + +static void print_help(void) +{ + cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts); + printf("\n"); + + printf("Display the content for the given object in the repository.\n"); + printf("\n"); + + printf("Options:\n"); + + cli_opt_help_fprint(stdout, opts); +} + +static int print_odb(git_object *object, display_t display) +{ + git_odb *odb = NULL; + git_odb_object *odb_object = NULL; + const unsigned char *content; + git_object_size_t size; + int ret = 0; + + /* + * Our parsed blobs retain the raw content; all other objects are + * parsed into a working representation. To get the raw content, + * we need to do an ODB lookup. (Thankfully, this should be cached + * in-memory from our last call.) + */ + if (git_object_type(object) == GIT_OBJECT_BLOB) { + content = git_blob_rawcontent((git_blob *)object); + size = git_blob_rawsize((git_blob *)object); + } else { + if (git_repository_odb(&odb, git_object_owner(object)) < 0 || + git_odb_read(&odb_object, odb, git_object_id(object)) < 0) { + ret = cli_error_git(); + goto done; + } + + content = git_odb_object_data(odb_object); + size = git_odb_object_size(odb_object); + } + + switch (display) { + case DISPLAY_SIZE: + if (printf("%" PRIu64 "\n", size) < 0) + ret = cli_error_os(); + break; + case DISPLAY_CONTENT: + if (p_write(fileno(stdout), content, (size_t)size) < 0) + ret = cli_error_os(); + break; + default: + GIT_ASSERT(0); + } + +done: + git_odb_object_free(odb_object); + git_odb_free(odb); + return ret; +} + +static int print_type(git_object *object) +{ + if (printf("%s\n", git_object_type2string(git_object_type(object))) < 0) + return cli_error_os(); + + return 0; +} + +static int print_pretty(git_object *object) +{ + const git_tree_entry *entry; + size_t i, count; + + /* + * Only trees are stored in an unreadable format and benefit from + * pretty-printing. + */ + if (git_object_type(object) != GIT_OBJECT_TREE) + return print_odb(object, DISPLAY_CONTENT); + + for (i = 0, count = git_tree_entrycount((git_tree *)object); i < count; i++) { + entry = git_tree_entry_byindex((git_tree *)object, i); + + if (printf("%06o %s %s\t%s\n", + git_tree_entry_filemode_raw(entry), + git_object_type2string(git_tree_entry_type(entry)), + git_oid_tostr_s(git_tree_entry_id(entry)), + git_tree_entry_name(entry)) < 0) + return cli_error_os(); + } + + return 0; +} + +int cmd_cat_file(int argc, char **argv) +{ + git_repository *repo = NULL; + git_object *object = NULL; + git_object_t type; + cli_opt invalid_opt; + int giterr, ret = 0; + + if (cli_opt_parse(&invalid_opt, opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU)) + return cli_opt_usage_error(COMMAND_NAME, opts, &invalid_opt); + + if (show_help) { + print_help(); + return 0; + } + + if (git_repository_open_ext(&repo, ".", GIT_REPOSITORY_OPEN_FROM_ENV, NULL) < 0) + return cli_error_git(); + + if ((giterr = git_revparse_single(&object, repo, object_spec)) < 0) { + if (display == DISPLAY_EXISTS && giterr == GIT_ENOTFOUND) + ret = 1; + else + ret = cli_error_git(); + + goto done; + } + + if (type_name) { + git_object *peeled; + + if ((type = git_object_string2type(type_name)) == GIT_OBJECT_INVALID) { + ret = cli_error_usage("invalid object type '%s'", type_name); + goto done; + } + + if (git_object_peel(&peeled, object, type) < 0) { + ret = cli_error_git(); + goto done; + } + + git_object_free(object); + object = peeled; + } + + switch (display) { + case DISPLAY_EXISTS: + ret = 0; + break; + case DISPLAY_TYPE: + ret = print_type(object); + break; + case DISPLAY_PRETTY: + ret = print_pretty(object); + break; + default: + ret = print_odb(object, display); + break; + } + +done: + git_object_free(object); + git_repository_free(repo); + return ret; +} diff --git a/src/cli/cmd_clone.c b/src/cli/cmd_clone.c new file mode 100644 index 0000000..e477625 --- /dev/null +++ b/src/cli/cmd_clone.c @@ -0,0 +1,192 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include +#include +#include "cli.h" +#include "cmd.h" +#include "error.h" +#include "sighandler.h" +#include "progress.h" + +#include "fs_path.h" +#include "futils.h" + +#define COMMAND_NAME "clone" + +static char *branch, *remote_path, *local_path, *depth; +static int show_help, quiet, checkout = 1, bare; +static bool local_path_exists; +static cli_progress progress = CLI_PROGRESS_INIT; + +static const cli_opt_spec opts[] = { + { CLI_OPT_TYPE_SWITCH, "help", 0, &show_help, 1, + CLI_OPT_USAGE_HIDDEN | CLI_OPT_USAGE_STOP_PARSING, NULL, + "display help about the " COMMAND_NAME " command" }, + + { CLI_OPT_TYPE_SWITCH, "quiet", 'q', &quiet, 1, + CLI_OPT_USAGE_DEFAULT, NULL, "display the type of the object" }, + { CLI_OPT_TYPE_SWITCH, "no-checkout", 'n', &checkout, 0, + CLI_OPT_USAGE_DEFAULT, NULL, "don't checkout HEAD" }, + { CLI_OPT_TYPE_SWITCH, "bare", 0, &bare, 1, + CLI_OPT_USAGE_DEFAULT, NULL, "don't create a working directory" }, + { CLI_OPT_TYPE_VALUE, "branch", 'b', &branch, 0, + CLI_OPT_USAGE_DEFAULT, "name", "branch to check out" }, + { CLI_OPT_TYPE_VALUE, "depth", 0, &depth, 0, + CLI_OPT_USAGE_DEFAULT, "depth", "commit depth to check out " }, + { CLI_OPT_TYPE_LITERAL }, + { CLI_OPT_TYPE_ARG, "repository", 0, &remote_path, 0, + CLI_OPT_USAGE_REQUIRED, "repository", "repository path" }, + { CLI_OPT_TYPE_ARG, "directory", 0, &local_path, 0, + CLI_OPT_USAGE_DEFAULT, "directory", "directory to clone into" }, + { 0 } +}; + +static void print_help(void) +{ + cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts); + printf("\n"); + + printf("Clone a repository into a new directory.\n"); + printf("\n"); + + printf("Options:\n"); + + cli_opt_help_fprint(stdout, opts); +} + +static char *compute_local_path(const char *orig_path) +{ + const char *slash; + char *local_path; + + if ((slash = strrchr(orig_path, '/')) == NULL && + (slash = strrchr(orig_path, '\\')) == NULL) + local_path = git__strdup(orig_path); + else + local_path = git__strdup(slash + 1); + + return local_path; +} + +static int compute_depth(const char *depth) +{ + int64_t i; + const char *endptr; + + if (!depth) + return 0; + + if (git__strntol64(&i, depth, strlen(depth), &endptr, 10) < 0 || i < 0 || i > INT_MAX || *endptr) { + fprintf(stderr, "fatal: depth '%s' is not valid.\n", depth); + exit(128); + } + + return (int)i; +} + +static bool validate_local_path(const char *path) +{ + if (!git_fs_path_exists(path)) + return false; + + if (!git_fs_path_isdir(path) || !git_fs_path_is_empty_dir(path)) { + fprintf(stderr, "fatal: destination path '%s' already exists and is not an empty directory.\n", + path); + exit(128); + } + + return true; +} + +static void cleanup(void) +{ + int rmdir_flags = GIT_RMDIR_REMOVE_FILES; + + cli_progress_abort(&progress); + + if (local_path_exists) + rmdir_flags |= GIT_RMDIR_SKIP_ROOT; + + if (!git_fs_path_isdir(local_path)) + return; + + git_futils_rmdir_r(local_path, NULL, rmdir_flags); +} + +static void interrupt_cleanup(void) +{ + cleanup(); + exit(130); +} + +int cmd_clone(int argc, char **argv) +{ + git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT; + git_repository *repo = NULL; + cli_opt invalid_opt; + char *computed_path = NULL; + int ret = 0; + + if (cli_opt_parse(&invalid_opt, opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU)) + return cli_opt_usage_error(COMMAND_NAME, opts, &invalid_opt); + + if (show_help) { + print_help(); + return 0; + } + + if (!remote_path) { + ret = cli_error_usage("you must specify a repository to clone"); + goto done; + } + + clone_opts.bare = !!bare; + clone_opts.checkout_branch = branch; + clone_opts.fetch_opts.depth = compute_depth(depth); + + if (!checkout) + clone_opts.checkout_opts.checkout_strategy = GIT_CHECKOUT_NONE; + + if (!local_path) + local_path = computed_path = compute_local_path(remote_path); + + local_path_exists = validate_local_path(local_path); + + cli_sighandler_set_interrupt(interrupt_cleanup); + + if (!local_path_exists && + git_futils_mkdir(local_path, 0777, 0) < 0) { + ret = cli_error_git(); + goto done; + } + + if (!quiet) { + clone_opts.fetch_opts.callbacks.sideband_progress = cli_progress_fetch_sideband; + clone_opts.fetch_opts.callbacks.transfer_progress = cli_progress_fetch_transfer; + clone_opts.fetch_opts.callbacks.payload = &progress; + + clone_opts.checkout_opts.progress_cb = cli_progress_checkout; + clone_opts.checkout_opts.progress_payload = &progress; + + printf("Cloning into '%s'...\n", local_path); + } + + if (git_clone(&repo, remote_path, local_path, &clone_opts) < 0) { + cleanup(); + ret = cli_error_git(); + goto done; + } + + cli_progress_finish(&progress); + +done: + cli_progress_dispose(&progress); + git__free(computed_path); + git_repository_free(repo); + return ret; +} diff --git a/src/cli/cmd_hash_object.c b/src/cli/cmd_hash_object.c new file mode 100644 index 0000000..93b980d --- /dev/null +++ b/src/cli/cmd_hash_object.c @@ -0,0 +1,154 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include +#include "cli.h" +#include "cmd.h" + +#include "futils.h" + +#define COMMAND_NAME "hash-object" + +static int show_help; +static char *type_name; +static int write_object, read_stdin, literally; +static char **filenames; + +static const cli_opt_spec opts[] = { + { CLI_OPT_TYPE_SWITCH, "help", 0, &show_help, 1, + CLI_OPT_USAGE_HIDDEN | CLI_OPT_USAGE_STOP_PARSING, NULL, + "display help about the " COMMAND_NAME " command" }, + + { CLI_OPT_TYPE_VALUE, NULL, 't', &type_name, 0, + CLI_OPT_USAGE_DEFAULT, "type", "the type of object to hash (default: \"blob\")" }, + { CLI_OPT_TYPE_SWITCH, NULL, 'w', &write_object, 1, + CLI_OPT_USAGE_DEFAULT, NULL, "write the object to the object database" }, + { CLI_OPT_TYPE_SWITCH, "literally", 0, &literally, 1, + CLI_OPT_USAGE_DEFAULT, NULL, "do not validate the object contents" }, + { CLI_OPT_TYPE_SWITCH, "stdin", 0, &read_stdin, 1, + CLI_OPT_USAGE_REQUIRED, NULL, "read content from stdin" }, + { CLI_OPT_TYPE_ARGS, "file", 0, &filenames, 0, + CLI_OPT_USAGE_CHOICE, "file", "the file (or files) to read and hash" }, + { 0 }, +}; + +static void print_help(void) +{ + cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts); + printf("\n"); + + printf("Compute the object ID for a given file and optionally write that file\nto the object database.\n"); + printf("\n"); + + printf("Options:\n"); + + cli_opt_help_fprint(stdout, opts); +} + +static int hash_buf( + git_odb *odb, + git_str *buf, + git_object_t object_type, + git_oid_t oid_type) +{ + git_oid oid; + + if (!literally) { + int valid = 0; + +#ifdef GIT_EXPERIMENTAL_SHA256 + if (git_object_rawcontent_is_valid(&valid, buf->ptr, buf->size, object_type, oid_type) < 0 || !valid) + return cli_error_git(); +#else + GIT_UNUSED(oid_type); + + if (git_object_rawcontent_is_valid(&valid, buf->ptr, buf->size, object_type) < 0 || !valid) + return cli_error_git(); +#endif + } + + if (write_object) { + if (git_odb_write(&oid, odb, buf->ptr, buf->size, object_type) < 0) + return cli_error_git(); + } else { +#ifdef GIT_EXPERIMENTAL_SHA256 + if (git_odb_hash(&oid, buf->ptr, buf->size, object_type, GIT_OID_SHA1) < 0) + return cli_error_git(); +#else + if (git_odb_hash(&oid, buf->ptr, buf->size, object_type) < 0) + return cli_error_git(); +#endif + } + + if (printf("%s\n", git_oid_tostr_s(&oid)) < 0) + return cli_error_os(); + + return 0; +} + +int cmd_hash_object(int argc, char **argv) +{ + git_repository *repo = NULL; + git_odb *odb = NULL; + git_oid_t oid_type; + git_str buf = GIT_STR_INIT; + cli_opt invalid_opt; + git_object_t object_type = GIT_OBJECT_BLOB; + char **filename; + int ret = 0; + + if (cli_opt_parse(&invalid_opt, opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU)) + return cli_opt_usage_error(COMMAND_NAME, opts, &invalid_opt); + + if (show_help) { + print_help(); + return 0; + } + + if (type_name && (object_type = git_object_string2type(type_name)) == GIT_OBJECT_INVALID) + return cli_error_usage("invalid object type '%s'", type_name); + + if (write_object && + (git_repository_open_ext(&repo, ".", GIT_REPOSITORY_OPEN_FROM_ENV, NULL) < 0 || + git_repository_odb(&odb, repo) < 0)) { + ret = cli_error_git(); + goto done; + } + + oid_type = git_repository_oid_type(repo); + + /* + * TODO: we're reading blobs, we shouldn't pull them all into main + * memory, we should just stream them into the odb instead. + * (Or create a `git_odb_writefile` API.) + */ + if (read_stdin) { + if (git_futils_readbuffer_fd_full(&buf, fileno(stdin)) < 0) { + ret = cli_error_git(); + goto done; + } + + if ((ret = hash_buf(odb, &buf, object_type, oid_type)) != 0) + goto done; + } else { + for (filename = filenames; *filename; filename++) { + if (git_futils_readbuffer(&buf, *filename) < 0) { + ret = cli_error_git(); + goto done; + } + + if ((ret = hash_buf(odb, &buf, object_type, oid_type)) != 0) + goto done; + } + } + +done: + git_str_dispose(&buf); + git_odb_free(odb); + git_repository_free(repo); + return ret; +} diff --git a/src/cli/cmd_help.c b/src/cli/cmd_help.c new file mode 100644 index 0000000..7ee9822 --- /dev/null +++ b/src/cli/cmd_help.c @@ -0,0 +1,86 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include +#include +#include "cli.h" +#include "cmd.h" + +#define COMMAND_NAME "help" + +static char *command; +static int show_help; + +static const cli_opt_spec opts[] = { + { CLI_OPT_TYPE_SWITCH, "help", 0, &show_help, 1, + CLI_OPT_USAGE_HIDDEN, NULL, "display help about the help command" }, + { CLI_OPT_TYPE_ARG, "command", 0, &command, 0, + CLI_OPT_USAGE_DEFAULT, "command", "the command to show help for" }, + { 0 }, +}; + +static int print_help(void) +{ + cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts); + printf("\n"); + + printf("Display help information about %s. If a command is specified, help\n", PROGRAM_NAME); + printf("about that command will be shown. Otherwise, general information about\n"); + printf("%s will be shown, including the commands available.\n", PROGRAM_NAME); + + return 0; +} + +static int print_commands(void) +{ + const cli_cmd_spec *cmd; + + cli_opt_usage_fprint(stdout, PROGRAM_NAME, NULL, cli_common_opts); + printf("\n"); + + printf("These are the %s commands available:\n\n", PROGRAM_NAME); + + for (cmd = cli_cmds; cmd->name; cmd++) + printf(" %-11s %s\n", cmd->name, cmd->desc); + + printf("\nSee '%s help ' for more information on a specific command.\n", PROGRAM_NAME); + + return 0; +} + +int cmd_help(int argc, char **argv) +{ + char *fake_args[2]; + const cli_cmd_spec *cmd; + cli_opt invalid_opt; + + if (cli_opt_parse(&invalid_opt, opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU)) + return cli_opt_usage_error(COMMAND_NAME, opts, &invalid_opt); + + /* Show the meta-help */ + if (show_help) + return print_help(); + + /* We were not asked to show help for a specific command. */ + if (!command) + return print_commands(); + + /* + * If we were asked for help for a command (eg, `help `), + * delegate back to that command's `--help` option. This lets + * commands own their help. Emulate the command-line arguments + * that would invoke ` --help` and invoke that command. + */ + fake_args[0] = command; + fake_args[1] = "--help"; + + if ((cmd = cli_cmd_spec_byname(command)) == NULL) + return cli_error("'%s' is not a %s command. See '%s help'.", + command, PROGRAM_NAME, PROGRAM_NAME); + + return cmd->fn(2, fake_args); +} diff --git a/src/cli/error.h b/src/cli/error.h new file mode 100644 index 0000000..cce7a54 --- /dev/null +++ b/src/cli/error.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef CLI_error_h__ +#define CLI_error_h__ + +#include "cli.h" +#include + +#define CLI_EXIT_OK 0 +#define CLI_EXIT_ERROR 1 +#define CLI_EXIT_OS 128 +#define CLI_EXIT_GIT 128 +#define CLI_EXIT_USAGE 129 + +#define cli_error__print(fmt) do { \ + va_list ap; \ + va_start(ap, fmt); \ + fprintf(stderr, "%s: ", PROGRAM_NAME); \ + vfprintf(stderr, fmt, ap); \ + fprintf(stderr, "\n"); \ + va_end(ap); \ + } while(0) + +GIT_INLINE(int) cli_error(const char *fmt, ...) +{ + cli_error__print(fmt); + return CLI_EXIT_ERROR; +} + +GIT_INLINE(int) cli_error_usage(const char *fmt, ...) +{ + cli_error__print(fmt); + return CLI_EXIT_USAGE; +} + +GIT_INLINE(int) cli_error_git(void) +{ + const git_error *err = git_error_last(); + fprintf(stderr, "%s: %s\n", PROGRAM_NAME, + err ? err->message : "unknown error"); + return CLI_EXIT_GIT; +} + +#define cli_error_os() (perror(PROGRAM_NAME), CLI_EXIT_OS) + +#endif /* CLI_error_h__ */ diff --git a/src/cli/main.c b/src/cli/main.c new file mode 100644 index 0000000..cbfc50e --- /dev/null +++ b/src/cli/main.c @@ -0,0 +1,106 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include +#include +#include "cli.h" +#include "cmd.h" + +static int show_help = 0; +static int show_version = 0; +static char *command = NULL; +static char **args = NULL; + +const cli_opt_spec cli_common_opts[] = { + { CLI_OPT_TYPE_SWITCH, "help", 0, &show_help, 1, + CLI_OPT_USAGE_DEFAULT, NULL, "display help information" }, + { CLI_OPT_TYPE_SWITCH, "version", 0, &show_version, 1, + CLI_OPT_USAGE_DEFAULT, NULL, "display the version" }, + { CLI_OPT_TYPE_ARG, "command", 0, &command, 0, + CLI_OPT_USAGE_REQUIRED, "command", "the command to run" }, + { CLI_OPT_TYPE_ARGS, "args", 0, &args, 0, + CLI_OPT_USAGE_DEFAULT, "args", "arguments for the command" }, + { 0 } +}; + +const cli_cmd_spec cli_cmds[] = { + { "cat-file", cmd_cat_file, "Display an object in the repository" }, + { "clone", cmd_clone, "Clone a repository into a new directory" }, + { "hash-object", cmd_hash_object, "Hash a raw object and product its object ID" }, + { "help", cmd_help, "Display help information" }, + { NULL } +}; + +int main(int argc, char **argv) +{ + const cli_cmd_spec *cmd; + cli_opt_parser optparser; + cli_opt opt; + char *help_args[3] = { NULL }; + int help_args_len; + int args_len = 0; + int ret = 0; + + if (git_libgit2_init() < 0) { + cli_error("failed to initialize libgit2"); + exit(CLI_EXIT_GIT); + } + + cli_opt_parser_init(&optparser, cli_common_opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU); + + /* Parse the top-level (common) options and command information */ + while (cli_opt_parser_next(&opt, &optparser)) { + if (!opt.spec) { + cli_opt_status_fprint(stderr, PROGRAM_NAME, &opt); + cli_opt_usage_fprint(stderr, PROGRAM_NAME, NULL, cli_common_opts); + ret = CLI_EXIT_USAGE; + goto done; + } + + /* + * When we see a command, stop parsing and capture the + * remaining arguments as args for the command itself. + */ + if (command) { + args = &argv[optparser.idx]; + args_len = (int)(argc - optparser.idx); + break; + } + } + + if (show_version) { + printf("%s version %s\n", PROGRAM_NAME, LIBGIT2_VERSION); + goto done; + } + + /* + * If `--help ` is specified, delegate to that command's + * `--help` option. If no command is specified, run the `help` + * command. Do this by updating the args to emulate that behavior. + */ + if (!command || show_help) { + help_args[0] = command ? (char *)command : "help"; + help_args[1] = command ? "--help" : NULL; + help_args_len = command ? 2 : 1; + + command = help_args[0]; + args = help_args; + args_len = help_args_len; + } + + if ((cmd = cli_cmd_spec_byname(command)) == NULL) { + ret = cli_error("'%s' is not a %s command. See '%s help'.", + command, PROGRAM_NAME, PROGRAM_NAME); + goto done; + } + + ret = cmd->fn(args_len, args); + +done: + git_libgit2_shutdown(); + return ret; +} diff --git a/src/cli/opt.c b/src/cli/opt.c new file mode 100644 index 0000000..62a3430 --- /dev/null +++ b/src/cli/opt.c @@ -0,0 +1,669 @@ +/* + * Copyright (c), Edward Thomson + * All rights reserved. + * + * This file is part of adopt, distributed under the MIT license. + * For full terms and conditions, see the included LICENSE file. + * + * THIS FILE IS AUTOMATICALLY GENERATED; DO NOT EDIT. + * + * This file was produced by using the `rename.pl` script included with + * adopt. The command-line specified was: + * + * ./rename.pl cli_opt --filename=opt --include=cli.h --inline=GIT_INLINE --header-guard=CLI_opt_h__ --lowercase-status --without-usage + */ + +#include +#include +#include +#include +#include + +#include "cli.h" +#include "opt.h" + +#ifdef _WIN32 +# include +#else +# include +# include +#endif + +#ifdef _MSC_VER +# define alloca _alloca +#endif + +#define spec_is_option_type(x) \ + ((x)->type == CLI_OPT_TYPE_BOOL || \ + (x)->type == CLI_OPT_TYPE_SWITCH || \ + (x)->type == CLI_OPT_TYPE_VALUE) + +GIT_INLINE(const cli_opt_spec *) spec_for_long( + int *is_negated, + int *has_value, + const char **value, + const cli_opt_parser *parser, + const char *arg) +{ + const cli_opt_spec *spec; + char *eql; + size_t eql_pos; + + eql = strchr(arg, '='); + eql_pos = (eql = strchr(arg, '=')) ? (size_t)(eql - arg) : strlen(arg); + + for (spec = parser->specs; spec->type; ++spec) { + /* Handle -- (everything after this is literal) */ + if (spec->type == CLI_OPT_TYPE_LITERAL && arg[0] == '\0') + return spec; + + /* Handle --no-option arguments for bool types */ + if (spec->type == CLI_OPT_TYPE_BOOL && + strncmp(arg, "no-", 3) == 0 && + strcmp(arg + 3, spec->name) == 0) { + *is_negated = 1; + return spec; + } + + /* Handle the typical --option arguments */ + if (spec_is_option_type(spec) && + spec->name && + strcmp(arg, spec->name) == 0) + return spec; + + /* Handle --option=value arguments */ + if (spec->type == CLI_OPT_TYPE_VALUE && + eql && + strncmp(arg, spec->name, eql_pos) == 0 && + spec->name[eql_pos] == '\0') { + *has_value = 1; + *value = arg[eql_pos + 1] ? &arg[eql_pos + 1] : NULL; + return spec; + } + } + + return NULL; +} + +GIT_INLINE(const cli_opt_spec *) spec_for_short( + const char **value, + const cli_opt_parser *parser, + const char *arg) +{ + const cli_opt_spec *spec; + + for (spec = parser->specs; spec->type; ++spec) { + /* Handle -svalue short options with a value */ + if (spec->type == CLI_OPT_TYPE_VALUE && + arg[0] == spec->alias && + arg[1] != '\0') { + *value = &arg[1]; + return spec; + } + + /* Handle typical -s short options */ + if (arg[0] == spec->alias) { + *value = NULL; + return spec; + } + } + + return NULL; +} + +GIT_INLINE(const cli_opt_spec *) spec_for_arg(cli_opt_parser *parser) +{ + const cli_opt_spec *spec; + size_t args = 0; + + for (spec = parser->specs; spec->type; ++spec) { + if (spec->type == CLI_OPT_TYPE_ARG) { + if (args == parser->arg_idx) { + parser->arg_idx++; + return spec; + } + + args++; + } + + if (spec->type == CLI_OPT_TYPE_ARGS && args == parser->arg_idx) + return spec; + } + + return NULL; +} + +GIT_INLINE(int) spec_is_choice(const cli_opt_spec *spec) +{ + return ((spec + 1)->type && + ((spec + 1)->usage & CLI_OPT_USAGE_CHOICE)); +} + +/* + * If we have a choice with switches and bare arguments, and we see + * the switch, then we no longer expect the bare argument. + */ +GIT_INLINE(void) consume_choices(const cli_opt_spec *spec, cli_opt_parser *parser) +{ + /* back up to the beginning of the choices */ + while (spec->type && (spec->usage & CLI_OPT_USAGE_CHOICE)) + --spec; + + if (!spec_is_choice(spec)) + return; + + do { + if (spec->type == CLI_OPT_TYPE_ARG) + parser->arg_idx++; + ++spec; + } while(spec->type && (spec->usage & CLI_OPT_USAGE_CHOICE)); +} + +static cli_opt_status_t parse_long(cli_opt *opt, cli_opt_parser *parser) +{ + const cli_opt_spec *spec; + char *arg = parser->args[parser->idx++]; + const char *value = NULL; + int is_negated = 0, has_value = 0; + + opt->arg = arg; + + if ((spec = spec_for_long(&is_negated, &has_value, &value, parser, &arg[2])) == NULL) { + opt->spec = NULL; + opt->status = CLI_OPT_STATUS_UNKNOWN_OPTION; + goto done; + } + + opt->spec = spec; + + /* Future options parsed as literal */ + if (spec->type == CLI_OPT_TYPE_LITERAL) + parser->in_literal = 1; + + /* --bool or --no-bool */ + else if (spec->type == CLI_OPT_TYPE_BOOL && spec->value) + *((int *)spec->value) = !is_negated; + + /* --accumulate */ + else if (spec->type == CLI_OPT_TYPE_ACCUMULATOR && spec->value) + *((int *)spec->value) += spec->switch_value ? spec->switch_value : 1; + + /* --switch */ + else if (spec->type == CLI_OPT_TYPE_SWITCH && spec->value) + *((int *)spec->value) = spec->switch_value; + + /* Parse values as "--foo=bar" or "--foo bar" */ + else if (spec->type == CLI_OPT_TYPE_VALUE) { + if (has_value) + opt->value = (char *)value; + else if ((parser->idx + 1) <= parser->args_len) + opt->value = parser->args[parser->idx++]; + + if (spec->value) + *((char **)spec->value) = opt->value; + } + + /* Required argument was not provided */ + if (spec->type == CLI_OPT_TYPE_VALUE && + !opt->value && + !(spec->usage & CLI_OPT_USAGE_VALUE_OPTIONAL)) + opt->status = CLI_OPT_STATUS_MISSING_VALUE; + else + opt->status = CLI_OPT_STATUS_OK; + + consume_choices(opt->spec, parser); + +done: + return opt->status; +} + +static cli_opt_status_t parse_short(cli_opt *opt, cli_opt_parser *parser) +{ + const cli_opt_spec *spec; + char *arg = parser->args[parser->idx++]; + const char *value; + + opt->arg = arg; + + if ((spec = spec_for_short(&value, parser, &arg[1 + parser->in_short])) == NULL) { + opt->spec = NULL; + opt->status = CLI_OPT_STATUS_UNKNOWN_OPTION; + goto done; + } + + opt->spec = spec; + + if (spec->type == CLI_OPT_TYPE_BOOL && spec->value) + *((int *)spec->value) = 1; + + else if (spec->type == CLI_OPT_TYPE_ACCUMULATOR && spec->value) + *((int *)spec->value) += spec->switch_value ? spec->switch_value : 1; + + else if (spec->type == CLI_OPT_TYPE_SWITCH && spec->value) + *((int *)spec->value) = spec->switch_value; + + /* Parse values as "-ifoo" or "-i foo" */ + else if (spec->type == CLI_OPT_TYPE_VALUE) { + if (value) + opt->value = (char *)value; + else if ((parser->idx + 1) <= parser->args_len) + opt->value = parser->args[parser->idx++]; + + if (spec->value) + *((char **)spec->value) = opt->value; + } + + /* + * Handle compressed short arguments, like "-fbcd"; see if there's + * another character after the one we processed. If not, advance + * the parser index. + */ + if (spec->type != CLI_OPT_TYPE_VALUE && arg[2 + parser->in_short] != '\0') { + parser->in_short++; + parser->idx--; + } else { + parser->in_short = 0; + } + + /* Required argument was not provided */ + if (spec->type == CLI_OPT_TYPE_VALUE && !opt->value) + opt->status = CLI_OPT_STATUS_MISSING_VALUE; + else + opt->status = CLI_OPT_STATUS_OK; + + consume_choices(opt->spec, parser); + +done: + return opt->status; +} + +static cli_opt_status_t parse_arg(cli_opt *opt, cli_opt_parser *parser) +{ + const cli_opt_spec *spec = spec_for_arg(parser); + + opt->spec = spec; + opt->arg = parser->args[parser->idx]; + + if (!spec) { + parser->idx++; + opt->status = CLI_OPT_STATUS_UNKNOWN_OPTION; + } else if (spec->type == CLI_OPT_TYPE_ARGS) { + if (spec->value) + *((char ***)spec->value) = &parser->args[parser->idx]; + + /* + * We have started a list of arguments; the remainder of + * given arguments need not be examined. + */ + parser->in_args = (parser->args_len - parser->idx); + parser->idx = parser->args_len; + opt->args_len = parser->in_args; + opt->status = CLI_OPT_STATUS_OK; + } else { + if (spec->value) + *((char **)spec->value) = parser->args[parser->idx]; + + parser->idx++; + opt->status = CLI_OPT_STATUS_OK; + } + + return opt->status; +} + +static int support_gnu_style(unsigned int flags) +{ + if ((flags & CLI_OPT_PARSE_FORCE_GNU) != 0) + return 1; + + if ((flags & CLI_OPT_PARSE_GNU) == 0) + return 0; + + /* TODO: Windows */ +#if defined(_WIN32) && defined(UNICODE) + if (_wgetenv(L"POSIXLY_CORRECT") != NULL) + return 0; +#else + if (getenv("POSIXLY_CORRECT") != NULL) + return 0; +#endif + + return 1; +} + +void cli_opt_parser_init( + cli_opt_parser *parser, + const cli_opt_spec specs[], + char **args, + size_t args_len, + unsigned int flags) +{ + assert(parser); + + memset(parser, 0x0, sizeof(cli_opt_parser)); + + parser->specs = specs; + parser->args = args; + parser->args_len = args_len; + parser->flags = flags; + + parser->needs_sort = support_gnu_style(flags); +} + +GIT_INLINE(const cli_opt_spec *) spec_for_sort( + int *needs_value, + const cli_opt_parser *parser, + const char *arg) +{ + int is_negated, has_value = 0; + const char *value; + const cli_opt_spec *spec = NULL; + size_t idx = 0; + + *needs_value = 0; + + if (strncmp(arg, "--", 2) == 0) { + spec = spec_for_long(&is_negated, &has_value, &value, parser, &arg[2]); + *needs_value = !has_value; + } + + else if (strncmp(arg, "-", 1) == 0) { + spec = spec_for_short(&value, parser, &arg[1]); + + /* + * Advance through compressed short arguments to see if + * the last one has a value, eg "-xvffilename". + */ + while (spec && !value && arg[1 + ++idx] != '\0') + spec = spec_for_short(&value, parser, &arg[1 + idx]); + + *needs_value = (value == NULL); + } + + return spec; +} + +/* + * Some parsers allow for handling arguments like "file1 --help file2"; + * this is done by re-sorting the arguments in-place; emulate that. + */ +static int sort_gnu_style(cli_opt_parser *parser) +{ + size_t i, j, insert_idx = parser->idx, offset; + const cli_opt_spec *spec; + char *option, *value; + int needs_value, changed = 0; + + parser->needs_sort = 0; + + for (i = parser->idx; i < parser->args_len; i++) { + spec = spec_for_sort(&needs_value, parser, parser->args[i]); + + /* Not a "-" or "--" prefixed option. No change. */ + if (!spec) + continue; + + /* A "--" alone means remaining args are literal. */ + if (spec->type == CLI_OPT_TYPE_LITERAL) + break; + + option = parser->args[i]; + + /* + * If the argument is a value type and doesn't already + * have a value (eg "--foo=bar" or "-fbar") then we need + * to copy the next argument as its value. + */ + if (spec->type == CLI_OPT_TYPE_VALUE && needs_value) { + /* + * A required value is not provided; set parser + * index to this value so that we fail on it. + */ + if (i + 1 >= parser->args_len) { + parser->idx = i; + return 1; + } + + value = parser->args[i + 1]; + offset = 1; + } else { + value = NULL; + offset = 0; + } + + /* Caller error if args[0] is an option. */ + if (i == 0) + return 0; + + /* Shift args up one (or two) and insert the option */ + for (j = i; j > insert_idx; j--) + parser->args[j + offset] = parser->args[j - 1]; + + parser->args[insert_idx] = option; + + if (value) + parser->args[insert_idx + 1] = value; + + insert_idx += (1 + offset); + i += offset; + + changed = 1; + } + + return changed; +} + +cli_opt_status_t cli_opt_parser_next(cli_opt *opt, cli_opt_parser *parser) +{ + assert(opt && parser); + + memset(opt, 0x0, sizeof(cli_opt)); + + if (parser->idx >= parser->args_len) { + opt->args_len = parser->in_args; + return CLI_OPT_STATUS_DONE; + } + + /* Handle options in long form, those beginning with "--" */ + if (strncmp(parser->args[parser->idx], "--", 2) == 0 && + !parser->in_short && + !parser->in_literal) + return parse_long(opt, parser); + + /* Handle options in short form, those beginning with "-" */ + else if (parser->in_short || + (strncmp(parser->args[parser->idx], "-", 1) == 0 && + !parser->in_literal)) + return parse_short(opt, parser); + + /* + * We've reached the first "bare" argument. In POSIX mode, all + * remaining items on the command line are arguments. In GNU + * mode, there may be long or short options after this. Sort any + * options up to this position then re-parse the current position. + */ + if (parser->needs_sort && sort_gnu_style(parser)) + return cli_opt_parser_next(opt, parser); + + return parse_arg(opt, parser); +} + +GIT_INLINE(int) spec_included(const cli_opt_spec **specs, const cli_opt_spec *spec) +{ + const cli_opt_spec **i; + + for (i = specs; *i; ++i) { + if (spec == *i) + return 1; + } + + return 0; +} + +static cli_opt_status_t validate_required( + cli_opt *opt, + const cli_opt_spec specs[], + const cli_opt_spec **given_specs) +{ + const cli_opt_spec *spec, *required; + int given; + + /* + * Iterate over the possible specs to identify requirements and + * ensure that those have been given on the command-line. + * Note that we can have required *choices*, where one in a + * list of choices must be specified. + */ + for (spec = specs, required = NULL, given = 0; spec->type; ++spec) { + if (!required && (spec->usage & CLI_OPT_USAGE_REQUIRED)) { + required = spec; + given = 0; + } else if (!required) { + continue; + } + + if (!given) + given = spec_included(given_specs, spec); + + /* + * Validate the requirement unless we're in a required + * choice. In that case, keep the required state and + * validate at the end of the choice list. + */ + if (!spec_is_choice(spec)) { + if (!given) { + opt->spec = required; + opt->status = CLI_OPT_STATUS_MISSING_ARGUMENT; + break; + } + + required = NULL; + given = 0; + } + } + + return opt->status; +} + +cli_opt_status_t cli_opt_parse( + cli_opt *opt, + const cli_opt_spec specs[], + char **args, + size_t args_len, + unsigned int flags) +{ + cli_opt_parser parser; + const cli_opt_spec **given_specs; + size_t given_idx = 0; + + cli_opt_parser_init(&parser, specs, args, args_len, flags); + + given_specs = alloca(sizeof(const cli_opt_spec *) * (args_len + 1)); + + while (cli_opt_parser_next(opt, &parser)) { + if (opt->status != CLI_OPT_STATUS_OK && + opt->status != CLI_OPT_STATUS_DONE) + return opt->status; + + if ((opt->spec->usage & CLI_OPT_USAGE_STOP_PARSING)) + return (opt->status = CLI_OPT_STATUS_DONE); + + given_specs[given_idx++] = opt->spec; + } + + given_specs[given_idx] = NULL; + + return validate_required(opt, specs, given_specs); +} + +static int spec_name_fprint(FILE *file, const cli_opt_spec *spec) +{ + int error; + + if (spec->type == CLI_OPT_TYPE_ARG) + error = fprintf(file, "%s", spec->value_name); + else if (spec->type == CLI_OPT_TYPE_ARGS) + error = fprintf(file, "%s", spec->value_name); + else if (spec->alias && !(spec->usage & CLI_OPT_USAGE_SHOW_LONG)) + error = fprintf(file, "-%c", spec->alias); + else + error = fprintf(file, "--%s", spec->name); + + return error; +} + +int cli_opt_status_fprint( + FILE *file, + const char *command, + const cli_opt *opt) +{ + const cli_opt_spec *choice; + int error; + + if (command && (error = fprintf(file, "%s: ", command)) < 0) + return error; + + switch (opt->status) { + case CLI_OPT_STATUS_DONE: + error = fprintf(file, "finished processing arguments (no error)\n"); + break; + case CLI_OPT_STATUS_OK: + error = fprintf(file, "no error\n"); + break; + case CLI_OPT_STATUS_UNKNOWN_OPTION: + error = fprintf(file, "unknown option: %s\n", opt->arg); + break; + case CLI_OPT_STATUS_MISSING_VALUE: + if ((error = fprintf(file, "argument '")) < 0 || + (error = spec_name_fprint(file, opt->spec)) < 0 || + (error = fprintf(file, "' requires a value.\n")) < 0) + break; + break; + case CLI_OPT_STATUS_MISSING_ARGUMENT: + if (spec_is_choice(opt->spec)) { + int is_choice = 1; + + if (spec_is_choice((opt->spec)+1)) + error = fprintf(file, "one of"); + else + error = fprintf(file, "either"); + + if (error < 0) + break; + + for (choice = opt->spec; is_choice; ++choice) { + is_choice = spec_is_choice(choice); + + if (!is_choice) + error = fprintf(file, " or"); + else if (choice != opt->spec) + error = fprintf(file, ","); + + if ((error < 0) || + (error = fprintf(file, " '")) < 0 || + (error = spec_name_fprint(file, choice)) < 0 || + (error = fprintf(file, "'")) < 0) + break; + + if (!spec_is_choice(choice)) + break; + } + + if ((error < 0) || + (error = fprintf(file, " is required.\n")) < 0) + break; + } else { + if ((error = fprintf(file, "argument '")) < 0 || + (error = spec_name_fprint(file, opt->spec)) < 0 || + (error = fprintf(file, "' is required.\n")) < 0) + break; + } + + break; + default: + error = fprintf(file, "unknown status: %d\n", opt->status); + break; + } + + return error; +} + diff --git a/src/cli/opt.h b/src/cli/opt.h new file mode 100644 index 0000000..6c1d460 --- /dev/null +++ b/src/cli/opt.h @@ -0,0 +1,349 @@ +/* + * Copyright (c), Edward Thomson + * All rights reserved. + * + * This file is part of adopt, distributed under the MIT license. + * For full terms and conditions, see the included LICENSE file. + * + * THIS FILE IS AUTOMATICALLY GENERATED; DO NOT EDIT. + * + * This file was produced by using the `rename.pl` script included with + * adopt. The command-line specified was: + * + * ./rename.pl cli_opt --filename=opt --include=cli.h --inline=GIT_INLINE --header-guard=CLI_opt_h__ --lowercase-status --without-usage + */ + +#ifndef CLI_opt_h__ +#define CLI_opt_h__ + +#include +#include + +/** + * The type of argument to be parsed. + */ +typedef enum { + CLI_OPT_TYPE_NONE = 0, + + /** + * An option that, when specified, sets a given value to true. + * This is useful for options like "--debug". A negation + * option (beginning with "no-") is implicitly specified; for + * example "--no-debug". The `value` pointer in the returned + * option will be set to `1` when this is specified, and set to + * `0` when the negation "no-" option is specified. + */ + CLI_OPT_TYPE_BOOL, + + /** + * An option that, when specified, sets the given `value` pointer + * to the specified `switch_value`. This is useful for booleans + * where you do not want the implicit negation that comes with an + * `CLI_OPT_TYPE_BOOL`, or for switches that multiplex a value, like + * setting a mode. For example, `--read` may set the `value` to + * `MODE_READ` and `--write` may set the `value` to `MODE_WRITE`. + */ + CLI_OPT_TYPE_SWITCH, + + /** + * An option that, when specified, increments the given + * `value` by the given `switch_value`. This can be specified + * multiple times to continue to increment the `value`. + * (For example, "-vvv" to set verbosity to 3.) + */ + CLI_OPT_TYPE_ACCUMULATOR, + + /** + * An option that takes a value, for example `-n value`, + * `-nvalue`, `--name value` or `--name=value`. + */ + CLI_OPT_TYPE_VALUE, + + /** + * A bare "--" that indicates that arguments following this are + * literal. This allows callers to specify things that might + * otherwise look like options, for example to operate on a file + * named "-rf" then you can invoke "program -- -rf" to treat + * "-rf" as an argument not an option. + */ + CLI_OPT_TYPE_LITERAL, + + /** + * A single argument, not an option. When options are exhausted, + * arguments will be matches in the order that they're specified + * in the spec list. For example, if two `CLI_OPT_TYPE_ARGS` are + * specified, `input_file` and `output_file`, then the first bare + * argument on the command line will be `input_file` and the + * second will be `output_file`. + */ + CLI_OPT_TYPE_ARG, + + /** + * A collection of arguments. This is useful when you want to take + * a list of arguments, for example, multiple paths. When specified, + * the value will be set to the first argument in the list. + */ + CLI_OPT_TYPE_ARGS, +} cli_opt_type_t; + +/** + * Additional information about an option, including parsing + * restrictions and usage information to be displayed to the end-user. + */ +typedef enum { + /** Defaults for the argument. */ + CLI_OPT_USAGE_DEFAULT = 0, + + /** This argument is required. */ + CLI_OPT_USAGE_REQUIRED = (1u << 0), + + /** + * This is a multiple choice argument, combined with the previous + * argument. For example, when the previous argument is `-f` and + * this optional is applied to an argument of type `-b` then one + * of `-f` or `-b` may be specified. + */ + CLI_OPT_USAGE_CHOICE = (1u << 1), + + /** + * This argument short-circuits the remainder of parsing. + * Useful for arguments like `--help`. + */ + CLI_OPT_USAGE_STOP_PARSING = (1u << 2), + + /** The argument's value is optional ("-n" or "-n foo") */ + CLI_OPT_USAGE_VALUE_OPTIONAL = (1u << 3), + + /** This argument should not be displayed in usage. */ + CLI_OPT_USAGE_HIDDEN = (1u << 4), + + /** In usage, show the long format instead of the abbreviated format. */ + CLI_OPT_USAGE_SHOW_LONG = (1u << 5), +} cli_opt_usage_t; + +typedef enum { + /** Default parsing behavior. */ + CLI_OPT_PARSE_DEFAULT = 0, + + /** + * Parse with GNU `getopt_long` style behavior, where options can + * be intermixed with arguments at any position (for example, + * "file1 --help file2".) Like `getopt_long`, this can mutate the + * arguments given. + */ + CLI_OPT_PARSE_GNU = (1u << 0), + + /** + * Force GNU `getopt_long` style behavior; the `POSIXLY_CORRECT` + * environment variable is ignored. + */ + CLI_OPT_PARSE_FORCE_GNU = (1u << 1), +} cli_opt_flag_t; + +/** Specification for an available option. */ +typedef struct cli_opt_spec { + /** Type of option expected. */ + cli_opt_type_t type; + + /** Name of the long option. */ + const char *name; + + /** The alias is the short (one-character) option alias. */ + const char alias; + + /** + * If this spec is of type `CLI_OPT_TYPE_BOOL`, this is a pointer + * to an `int` that will be set to `1` if the option is specified. + * + * If this spec is of type `CLI_OPT_TYPE_SWITCH`, this is a pointer + * to an `int` that will be set to the opt's `switch_value` (below) + * when this option is specified. + * + * If this spec is of type `CLI_OPT_TYPE_ACCUMULATOR`, this is a + * pointer to an `int` that will be incremented by the opt's + * `switch_value` (below). If no `switch_value` is provided then + * the value will be incremented by 1. + * + * If this spec is of type `CLI_OPT_TYPE_VALUE`, + * `CLI_OPT_TYPE_VALUE_OPTIONAL`, or `CLI_OPT_TYPE_ARG`, this is + * a pointer to a `char *` that will be set to the value + * specified on the command line. + * + * If this spec is of type `CLI_OPT_TYPE_ARGS`, this is a pointer + * to a `char **` that will be set to the remaining values + * specified on the command line. + */ + void *value; + + /** + * If this spec is of type `CLI_OPT_TYPE_SWITCH`, this is the value + * to set in the option's `value` pointer when it is specified. If + * this spec is of type `CLI_OPT_TYPE_ACCUMULATOR`, this is the value + * to increment in the option's `value` pointer when it is + * specified. This is ignored for other opt types. + */ + int switch_value; + + /** + * Optional usage flags that change parsing behavior and how + * usage information is shown to the end-user. + */ + uint32_t usage; + + /** + * The name of the value, provided when creating usage information. + * This is required only for the functions that display usage + * information and only when a spec is of type `CLI_OPT_TYPE_VALUE, + * `CLI_OPT_TYPE_ARG` or `CLI_OPT_TYPE_ARGS``. + */ + const char *value_name; + + /** + * Optional short description of the option to display to the + * end-user. This is only used when creating usage information. + */ + const char *help; +} cli_opt_spec; + +/** Return value for `cli_opt_parser_next`. */ +typedef enum { + /** Parsing is complete; there are no more arguments. */ + CLI_OPT_STATUS_DONE = 0, + + /** + * This argument was parsed correctly; the `opt` structure is + * populated and the value pointer has been set. + */ + CLI_OPT_STATUS_OK = 1, + + /** + * The argument could not be parsed correctly, it does not match + * any of the specifications provided. + */ + CLI_OPT_STATUS_UNKNOWN_OPTION = 2, + + /** + * The argument matched a spec of type `CLI_OPT_VALUE`, but no value + * was provided. + */ + CLI_OPT_STATUS_MISSING_VALUE = 3, + + /** A required argument was not provided. */ + CLI_OPT_STATUS_MISSING_ARGUMENT = 4, +} cli_opt_status_t; + +/** An option provided on the command-line. */ +typedef struct cli_opt { + /** The status of parsing the most recent argument. */ + cli_opt_status_t status; + + /** + * The specification that was provided on the command-line, or + * `NULL` if the argument did not match an `cli_opt_spec`. + */ + const cli_opt_spec *spec; + + /** + * The argument as it was specified on the command-line, including + * dashes, eg, `-f` or `--foo`. + */ + char *arg; + + /** + * If the spec is of type `CLI_OPT_VALUE` or `CLI_OPT_VALUE_OPTIONAL`, + * this is the value provided to the argument. + */ + char *value; + + /** + * If the argument is of type `CLI_OPT_ARGS`, this is the number of + * arguments remaining. This value is persisted even when parsing + * is complete and `status` == `CLI_OPT_STATUS_DONE`. + */ + size_t args_len; +} cli_opt; + +/* The internal parser state. Callers should not modify this structure. */ +typedef struct cli_opt_parser { + const cli_opt_spec *specs; + char **args; + size_t args_len; + unsigned int flags; + + /* Parser state */ + size_t idx; + size_t arg_idx; + size_t in_args; + size_t in_short; + int needs_sort : 1, + in_literal : 1; +} cli_opt_parser; + +/** + * Parses all the command-line arguments and updates all the options using + * the pointers provided. Parsing stops on any invalid argument and + * information about the failure will be provided in the opt argument. + * + * This is the simplest way to parse options; it handles the initialization + * (`parser_init`) and looping (`parser_next`). + * + * @param opt The The `cli_opt` information that failed parsing + * @param specs A NULL-terminated array of `cli_opt_spec`s that can be parsed + * @param args The arguments that will be parsed + * @param args_len The length of arguments to be parsed + * @param flags The `cli_opt_flag_t flags for parsing + */ +cli_opt_status_t cli_opt_parse( + cli_opt *opt, + const cli_opt_spec specs[], + char **args, + size_t args_len, + unsigned int flags); + +/** + * Initializes a parser that parses the given arguments according to the + * given specifications. + * + * @param parser The `cli_opt_parser` that will be initialized + * @param specs A NULL-terminated array of `cli_opt_spec`s that can be parsed + * @param args The arguments that will be parsed + * @param args_len The length of arguments to be parsed + * @param flags The `cli_opt_flag_t flags for parsing + */ +void cli_opt_parser_init( + cli_opt_parser *parser, + const cli_opt_spec specs[], + char **args, + size_t args_len, + unsigned int flags); + +/** + * Parses the next command-line argument and places the information about + * the argument into the given `opt` data. + * + * @param opt The `cli_opt` information parsed from the argument + * @param parser An `cli_opt_parser` that has been initialized with + * `cli_opt_parser_init` + * @return true if the caller should continue iterating, or 0 if there are + * no arguments left to process. + */ +cli_opt_status_t cli_opt_parser_next( + cli_opt *opt, + cli_opt_parser *parser); + +/** + * Prints the status after parsing the most recent argument. This is + * useful for printing an error message when an unknown argument was + * specified, or when an argument was specified without a value. + * + * @param file The file to print information to + * @param command The name of the command to use when printing (optional) + * @param opt The option that failed to parse + * @return 0 on success, -1 on failure + */ +int cli_opt_status_fprint( + FILE *file, + const char *command, + const cli_opt *opt); + +#endif /* CLI_opt_h__ */ diff --git a/src/cli/opt_usage.c b/src/cli/opt_usage.c new file mode 100644 index 0000000..478b416 --- /dev/null +++ b/src/cli/opt_usage.c @@ -0,0 +1,194 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "cli.h" +#include "str.h" + +static int print_spec_name(git_str *out, const cli_opt_spec *spec) +{ + if (spec->type == CLI_OPT_TYPE_VALUE && spec->alias && + !(spec->usage & CLI_OPT_USAGE_VALUE_OPTIONAL) && + !(spec->usage & CLI_OPT_USAGE_SHOW_LONG)) + return git_str_printf(out, "-%c <%s>", spec->alias, spec->value_name); + if (spec->type == CLI_OPT_TYPE_VALUE && spec->alias && + !(spec->usage & CLI_OPT_USAGE_SHOW_LONG)) + return git_str_printf(out, "-%c [<%s>]", spec->alias, spec->value_name); + if (spec->type == CLI_OPT_TYPE_VALUE && + !(spec->usage & CLI_OPT_USAGE_VALUE_OPTIONAL)) + return git_str_printf(out, "--%s[=<%s>]", spec->name, spec->value_name); + if (spec->type == CLI_OPT_TYPE_VALUE) + return git_str_printf(out, "--%s=<%s>", spec->name, spec->value_name); + if (spec->type == CLI_OPT_TYPE_ARG) + return git_str_printf(out, "<%s>", spec->value_name); + if (spec->type == CLI_OPT_TYPE_ARGS) + return git_str_printf(out, "<%s>...", spec->value_name); + if (spec->type == CLI_OPT_TYPE_LITERAL) + return git_str_printf(out, "--"); + if (spec->alias && !(spec->usage & CLI_OPT_USAGE_SHOW_LONG)) + return git_str_printf(out, "-%c", spec->alias); + if (spec->name) + return git_str_printf(out, "--%s", spec->name); + + GIT_ASSERT(0); +} + +/* + * This is similar to adopt's function, but modified to understand + * that we have a command ("git") and a "subcommand" ("checkout"). + * It also understands a terminal's line length and wrap appropriately, + * using a `git_str` for storage. + */ +int cli_opt_usage_fprint( + FILE *file, + const char *command, + const char *subcommand, + const cli_opt_spec specs[]) +{ + git_str usage = GIT_BUF_INIT, opt = GIT_BUF_INIT; + const cli_opt_spec *spec; + size_t i, prefixlen, linelen; + bool choice = false, next_choice = false, optional = false; + int error; + + /* TODO: query actual console width. */ + int console_width = 80; + + if ((error = git_str_printf(&usage, "usage: %s", command)) < 0) + goto done; + + if (subcommand && + (error = git_str_printf(&usage, " %s", subcommand)) < 0) + goto done; + + linelen = git_str_len(&usage); + prefixlen = linelen + 1; + + for (spec = specs; spec->type; ++spec) { + if (!choice) + optional = !(spec->usage & CLI_OPT_USAGE_REQUIRED); + + next_choice = !!((spec + 1)->usage & CLI_OPT_USAGE_CHOICE); + + if (spec->usage & CLI_OPT_USAGE_HIDDEN) + continue; + + if (choice) + git_str_putc(&opt, '|'); + else + git_str_clear(&opt); + + if (optional && !choice) + git_str_putc(&opt, '['); + if (!optional && !choice && next_choice) + git_str_putc(&opt, '('); + + if ((error = print_spec_name(&opt, spec)) < 0) + goto done; + + if (!optional && choice && !next_choice) + git_str_putc(&opt, ')'); + else if (optional && !next_choice) + git_str_putc(&opt, ']'); + + if ((choice = next_choice)) + continue; + + if (git_str_oom(&opt)) { + error = -1; + goto done; + } + + if (linelen > prefixlen && + console_width > 0 && + linelen + git_str_len(&opt) + 1 > (size_t)console_width) { + git_str_putc(&usage, '\n'); + + for (i = 0; i < prefixlen; i++) + git_str_putc(&usage, ' '); + + linelen = prefixlen; + } else { + git_str_putc(&usage, ' '); + linelen += git_str_len(&opt) + 1; + } + + git_str_puts(&usage, git_str_cstr(&opt)); + + if (git_str_oom(&usage)) { + error = -1; + goto done; + } + } + + error = fprintf(file, "%s\n", git_str_cstr(&usage)); + +done: + error = (error < 0) ? -1 : 0; + + git_str_dispose(&usage); + git_str_dispose(&opt); + return error; +} + +int cli_opt_usage_error( + const char *subcommand, + const cli_opt_spec specs[], + const cli_opt *invalid_opt) +{ + cli_opt_status_fprint(stderr, PROGRAM_NAME, invalid_opt); + cli_opt_usage_fprint(stderr, PROGRAM_NAME, subcommand, specs); + return CLI_EXIT_USAGE; +} + +int cli_opt_help_fprint( + FILE *file, + const cli_opt_spec specs[]) +{ + git_str help = GIT_BUF_INIT; + const cli_opt_spec *spec; + int error = 0; + + /* Display required arguments first */ + for (spec = specs; spec->type; ++spec) { + if (! (spec->usage & CLI_OPT_USAGE_REQUIRED) || + (spec->usage & CLI_OPT_USAGE_HIDDEN)) + continue; + + git_str_printf(&help, " "); + + if ((error = print_spec_name(&help, spec)) < 0) + goto done; + + git_str_printf(&help, ": %s\n", spec->help); + } + + /* Display the remaining arguments */ + for (spec = specs; spec->type; ++spec) { + if ((spec->usage & CLI_OPT_USAGE_REQUIRED) || + (spec->usage & CLI_OPT_USAGE_HIDDEN)) + continue; + + git_str_printf(&help, " "); + + if ((error = print_spec_name(&help, spec)) < 0) + goto done; + + git_str_printf(&help, ": %s\n", spec->help); + + } + + if (git_str_oom(&help) || + p_write(fileno(file), help.ptr, help.size) < 0) + error = -1; + +done: + error = (error < 0) ? -1 : 0; + + git_str_dispose(&help); + return error; +} + diff --git a/src/cli/opt_usage.h b/src/cli/opt_usage.h new file mode 100644 index 0000000..c752494 --- /dev/null +++ b/src/cli/opt_usage.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef CLI_opt_usage_h__ +#define CLI_opt_usage_h__ + +/** + * Prints usage information to the given file handle. + * + * @param file The file to print information to + * @param command The name of the command to use when printing + * @param subcommand The name of the subcommand (eg "checkout") to use when printing, or NULL to skip + * @param specs The specifications allowed by the command + * @return 0 on success, -1 on failure + */ +int cli_opt_usage_fprint( + FILE *file, + const char *command, + const char *subcommand, + const cli_opt_spec specs[]); + +int cli_opt_usage_error( + const char *subcommand, + const cli_opt_spec specs[], + const cli_opt *invalid_opt); + +int cli_opt_help_fprint( + FILE *file, + const cli_opt_spec specs[]); + +#endif /* CLI_opt_usage_h__ */ diff --git a/src/cli/progress.c b/src/cli/progress.c new file mode 100644 index 0000000..ddfbafb --- /dev/null +++ b/src/cli/progress.c @@ -0,0 +1,346 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include +#include +#include + +#include "progress.h" +#include "error.h" + +/* + * Show updates to the percentage and number of objects received + * separately from the throughput to give an accurate progress while + * avoiding too much noise on the screen. (In milliseconds.) + */ +#define PROGRESS_UPDATE_TIME 60 +#define THROUGHPUT_UPDATE_TIME 500 + +#define is_nl(c) ((c) == '\r' || (c) == '\n') + +#define return_os_error(msg) do { \ + git_error_set(GIT_ERROR_OS, "%s", msg); return -1; } while(0) + +GIT_INLINE(size_t) no_nl_len(const char *str, size_t len) +{ + size_t i = 0; + + while (i < len && !is_nl(str[i])) + i++; + + return i; +} + +GIT_INLINE(size_t) nl_len(bool *has_nl, const char *str, size_t len) +{ + size_t i = no_nl_len(str, len); + + *has_nl = false; + + while (i < len && is_nl(str[i])) { + *has_nl = true; + i++; + } + + return i; +} + +static int progress_write(cli_progress *progress, bool force, git_str *line) +{ + bool has_nl; + size_t no_nl = no_nl_len(line->ptr, line->size); + size_t nl = nl_len(&has_nl, line->ptr + no_nl, line->size - no_nl); + uint64_t now = git_time_monotonic(); + size_t i; + + /* Avoid spamming the console with progress updates */ + if (!force && line->ptr[line->size - 1] != '\n' && progress->last_update) { + if (now - progress->last_update < PROGRESS_UPDATE_TIME) { + git_str_clear(&progress->deferred); + git_str_put(&progress->deferred, line->ptr, line->size); + return git_str_oom(&progress->deferred) ? -1 : 0; + } + } + + /* + * If there's something on this line already (eg, a progress line + * with only a trailing `\r` that we'll print over) then we need + * to really print over it in case we're writing a shorter line. + */ + if (printf("%.*s", (int)no_nl, line->ptr) < 0) + return_os_error("could not print status"); + + if (progress->onscreen.size) { + for (i = no_nl; i < progress->onscreen.size; i++) { + if (printf(" ") < 0) + return_os_error("could not print status"); + } + } + + if (printf("%.*s", (int)nl, line->ptr + no_nl) < 0 || + fflush(stdout) != 0) + return_os_error("could not print status"); + + git_str_clear(&progress->onscreen); + + if (line->ptr[line->size - 1] == '\n') { + progress->last_update = 0; + } else { + git_str_put(&progress->onscreen, line->ptr, line->size); + progress->last_update = now; + } + + git_str_clear(&progress->deferred); + return git_str_oom(&progress->onscreen) ? -1 : 0; +} + +static int progress_printf(cli_progress *progress, bool force, const char *fmt, ...) + GIT_FORMAT_PRINTF(3, 4); + +int progress_printf(cli_progress *progress, bool force, const char *fmt, ...) +{ + git_str buf = GIT_BUF_INIT; + va_list ap; + int error; + + va_start(ap, fmt); + error = git_str_vprintf(&buf, fmt, ap); + va_end(ap); + + if (error < 0) + return error; + + error = progress_write(progress, force, &buf); + + git_str_dispose(&buf); + return error; +} + +static int progress_complete(cli_progress *progress) +{ + if (progress->deferred.size) + progress_write(progress, true, &progress->deferred); + + if (progress->onscreen.size) + if (printf("\n") < 0) + return_os_error("could not print status"); + + git_str_clear(&progress->deferred); + git_str_clear(&progress->onscreen); + progress->last_update = 0; + progress->action_start = 0; + progress->action_finish = 0; + + return 0; +} + +GIT_INLINE(int) percent(size_t completed, size_t total) +{ + if (total == 0) + return (completed == 0) ? 100 : 0; + + return (int)(((double)completed / (double)total) * 100); +} + +int cli_progress_fetch_sideband(const char *str, int len, void *payload) +{ + cli_progress *progress = (cli_progress *)payload; + size_t remain; + + if (len <= 0) + return 0; + + /* Accumulate the sideband data, then print it line-at-a-time. */ + if (git_str_put(&progress->sideband, str, len) < 0) + return -1; + + str = progress->sideband.ptr; + remain = progress->sideband.size; + + while (remain) { + bool has_nl; + size_t line_len = nl_len(&has_nl, str, remain); + + if (!has_nl) + break; + + if (line_len < INT_MAX) { + int error = progress_printf(progress, true, + "remote: %.*s", (int)line_len, str); + + if (error < 0) + return error; + } + + str += line_len; + remain -= line_len; + } + + git_str_consume_bytes(&progress->sideband, (progress->sideband.size - remain)); + + return 0; +} + +static int fetch_receiving( + cli_progress *progress, + const git_indexer_progress *stats) +{ + char *recv_units[] = { "B", "KiB", "MiB", "GiB", "TiB", NULL }; + char *rate_units[] = { "B/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s", NULL }; + uint64_t now, elapsed; + + double recv_len, rate; + size_t recv_unit_idx = 0, rate_unit_idx = 0; + bool done = (stats->received_objects == stats->total_objects); + + if (!progress->action_start) + progress->action_start = git_time_monotonic(); + + if (done && progress->action_finish) + now = progress->action_finish; + else if (done) + progress->action_finish = now = git_time_monotonic(); + else + now = git_time_monotonic(); + + if (progress->throughput_update && + now - progress->throughput_update < THROUGHPUT_UPDATE_TIME) { + elapsed = progress->throughput_update - + progress->action_start; + recv_len = progress->throughput_bytes; + } else { + elapsed = now - progress->action_start; + recv_len = (double)stats->received_bytes; + + progress->throughput_update = now; + progress->throughput_bytes = recv_len; + } + + rate = elapsed ? recv_len / elapsed : 0; + + while (recv_len > 1024 && recv_units[recv_unit_idx+1]) { + recv_len /= 1024; + recv_unit_idx++; + } + + while (rate > 1024 && rate_units[rate_unit_idx+1]) { + rate /= 1024; + rate_unit_idx++; + } + + return progress_printf(progress, false, + "Receiving objects: %3d%% (%d/%d), %.2f %s | %.2f %s%s\r", + percent(stats->received_objects, stats->total_objects), + stats->received_objects, + stats->total_objects, + recv_len, recv_units[recv_unit_idx], + rate, rate_units[rate_unit_idx], + done ? ", done." : ""); +} + +static int fetch_resolving( + cli_progress *progress, + const git_indexer_progress *stats) +{ + bool done = (stats->indexed_deltas == stats->total_deltas); + + return progress_printf(progress, false, + "Resolving deltas: %3d%% (%d/%d)%s\r", + percent(stats->indexed_deltas, stats->total_deltas), + stats->indexed_deltas, stats->total_deltas, + done ? ", done." : ""); +} + +int cli_progress_fetch_transfer(const git_indexer_progress *stats, void *payload) +{ + cli_progress *progress = (cli_progress *)payload; + int error = 0; + + switch (progress->action) { + case CLI_PROGRESS_NONE: + progress->action = CLI_PROGRESS_RECEIVING; + /* fall through */ + + case CLI_PROGRESS_RECEIVING: + if ((error = fetch_receiving(progress, stats)) < 0) + break; + + /* + * Upgrade from receiving to resolving; do this after the + * final call to cli_progress_fetch_receiving (above) to + * ensure that we've printed a final "done" string after + * any sideband data. + */ + if (!stats->indexed_deltas) + break; + + progress_complete(progress); + progress->action = CLI_PROGRESS_RESOLVING; + /* fall through */ + + case CLI_PROGRESS_RESOLVING: + error = fetch_resolving(progress, stats); + break; + + default: + /* should not be reached */ + GIT_ASSERT(!"unexpected progress state"); + } + + return error; +} + +void cli_progress_checkout( + const char *path, + size_t completed_steps, + size_t total_steps, + void *payload) +{ + cli_progress *progress = (cli_progress *)payload; + bool done = (completed_steps == total_steps); + + GIT_UNUSED(path); + + if (progress->action != CLI_PROGRESS_CHECKING_OUT) { + progress_complete(progress); + progress->action = CLI_PROGRESS_CHECKING_OUT; + } + + progress_printf(progress, false, + "Checking out files: %3d%% (%" PRIuZ "/%" PRIuZ ")%s\r", + percent(completed_steps, total_steps), + completed_steps, total_steps, + done ? ", done." : ""); +} + +int cli_progress_abort(cli_progress *progress) +{ + if (progress->onscreen.size > 0 && printf("\n") < 0) + return_os_error("could not print status"); + + return 0; +} + +int cli_progress_finish(cli_progress *progress) +{ + int error = progress->action ? progress_complete(progress) : 0; + + progress->action = 0; + return error; +} + +void cli_progress_dispose(cli_progress *progress) +{ + if (progress == NULL) + return; + + git_str_dispose(&progress->sideband); + git_str_dispose(&progress->onscreen); + git_str_dispose(&progress->deferred); + + memset(progress, 0, sizeof(cli_progress)); +} diff --git a/src/cli/progress.h b/src/cli/progress.h new file mode 100644 index 0000000..886fef8 --- /dev/null +++ b/src/cli/progress.h @@ -0,0 +1,117 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef CLI_progress_h__ +#define CLI_progress_h__ + +#include +#include "str.h" + +/* + * A general purpose set of progress printing functions. An individual + * `cli_progress` object is capable of displaying progress for a single + * function, even if that function displays multiple pieces of progress + * (like `git_clone`). `cli_progress_finish` should be called after + * any function invocation to re-set state. + */ + +typedef enum { + CLI_PROGRESS_NONE, + CLI_PROGRESS_RECEIVING, + CLI_PROGRESS_RESOLVING, + CLI_PROGRESS_CHECKING_OUT +} cli_progress_t; + +typedef struct { + cli_progress_t action; + + /* Actions may time themselves (eg fetch) but are not required to */ + uint64_t action_start; + uint64_t action_finish; + + /* Last console update, avoid too frequent updates. */ + uint64_t last_update; + + /* Accumulators for partial output and deferred updates. */ + git_str sideband; + git_str onscreen; + git_str deferred; + + /* Last update about throughput */ + uint64_t throughput_update; + double throughput_bytes; +} cli_progress; + +#define CLI_PROGRESS_INIT { 0 } + +/** + * Prints sideband data from fetch to the console. Suitable for a + * `sideband_progress` callback for `git_fetch_options`. + * + * @param str The sideband string + * @param len The length of the sideband string + * @param payload A pointer to the cli_progress + * @return 0 on success, -1 on failure + */ +extern int cli_progress_fetch_sideband( + const char *str, + int len, + void *payload); + +/** + * Prints fetch transfer statistics to the console. Suitable for a + * `transfer_progress` callback for `git_fetch_options`. + * + * @param stats The indexer stats + * @param payload A pointer to the cli_progress + * @return 0 on success, -1 on failure + */ +extern int cli_progress_fetch_transfer( + const git_indexer_progress *stats, + void *payload); + +/** + * Prints checkout progress to the console. Suitable for a + * `progress_cb` callback for `git_checkout_options`. + * + * @param path The path being written + * @param completed_steps The completed checkout steps + * @param total_steps The total number of checkout steps + * @param payload A pointer to the cli_progress + */ +extern void cli_progress_checkout( + const char *path, + size_t completed_steps, + size_t total_steps, + void *payload); + +/** + * Stop displaying progress quickly; suitable for stopping an application + * quickly. Does not display any lines that were buffered, just gets the + * console back to a sensible place. + * + * @param progress The progress information + * @return 0 on success, -1 on failure + */ +extern int cli_progress_abort(cli_progress *progress); + +/** + * Finishes displaying progress; flushes any buffered output. + * + * @param progress The progress information + * @return 0 on success, -1 on failure + */ +extern int cli_progress_finish(cli_progress *progress); + +/** + * Disposes the progress information. + * + * @param progress The progress information + */ +extern void cli_progress_dispose(cli_progress *progress); + +#endif /* CLI_progress_h__ */ diff --git a/src/cli/sighandler.h b/src/cli/sighandler.h new file mode 100644 index 0000000..877223e --- /dev/null +++ b/src/cli/sighandler.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef CLI_sighandler_h__ +#define CLI_sighandler_h__ + +/** + * Sets up a signal handler that will run when the process is interrupted + * (via SIGINT on POSIX or Control-C or Control-Break on Windows). + * + * @param handler The function to run on interrupt + * @return 0 on success, -1 on failure + */ +int cli_sighandler_set_interrupt(void (*handler)(void)); + +#endif /* CLI_sighandler_h__ */ diff --git a/src/cli/unix/sighandler.c b/src/cli/unix/sighandler.c new file mode 100644 index 0000000..6b4982d --- /dev/null +++ b/src/cli/unix/sighandler.c @@ -0,0 +1,36 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include +#include +#include "git2_util.h" +#include "cli.h" + +static void (*interrupt_handler)(void) = NULL; + +static void interrupt_proxy(int signal) +{ + GIT_UNUSED(signal); + interrupt_handler(); +} + +int cli_sighandler_set_interrupt(void (*handler)(void)) +{ + void (*result)(int); + + if ((interrupt_handler = handler) != NULL) + result = signal(SIGINT, interrupt_proxy); + else + result = signal(SIGINT, SIG_DFL); + + if (result == SIG_ERR) { + git_error_set(GIT_ERROR_OS, "could not set signal handler"); + return -1; + } + + return 0; +} diff --git a/src/cli/win32/precompiled.c b/src/cli/win32/precompiled.c new file mode 100644 index 0000000..5f656a4 --- /dev/null +++ b/src/cli/win32/precompiled.c @@ -0,0 +1 @@ +#include "precompiled.h" diff --git a/src/cli/win32/precompiled.h b/src/cli/win32/precompiled.h new file mode 100644 index 0000000..b0309b8 --- /dev/null +++ b/src/cli/win32/precompiled.h @@ -0,0 +1,3 @@ +#include + +#include "cli.h" diff --git a/src/cli/win32/sighandler.c b/src/cli/win32/sighandler.c new file mode 100644 index 0000000..cc0b646 --- /dev/null +++ b/src/cli/win32/sighandler.c @@ -0,0 +1,37 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2_util.h" +#include + +#include "cli.h" + +static void (*interrupt_handler)(void) = NULL; + +static BOOL WINAPI interrupt_proxy(DWORD signal) +{ + GIT_UNUSED(signal); + interrupt_handler(); + return TRUE; +} + +int cli_sighandler_set_interrupt(void (*handler)(void)) +{ + BOOL result; + + if ((interrupt_handler = handler) != NULL) + result = SetConsoleCtrlHandler(interrupt_proxy, FALSE); + else + result = SetConsoleCtrlHandler(NULL, FALSE); + + if (!result) { + git_error_set(GIT_ERROR_OS, "could not set control control handler"); + return -1; + } + + return 0; +} diff --git a/src/libgit2/CMakeLists.txt b/src/libgit2/CMakeLists.txt new file mode 100644 index 0000000..876a703 --- /dev/null +++ b/src/libgit2/CMakeLists.txt @@ -0,0 +1,122 @@ +# libgit2: the shared library: this CMakeLists.txt compiles the core +# git library functionality. + +add_library(libgit2 OBJECT) +set_target_properties(libgit2 PROPERTIES C_STANDARD 90) +set_target_properties(libgit2 PROPERTIES C_EXTENSIONS OFF) + +include(PkgBuildConfig) + +set(LIBGIT2_INCLUDES + "${PROJECT_BINARY_DIR}/src/util" + "${PROJECT_BINARY_DIR}/include" + "${PROJECT_SOURCE_DIR}/src/libgit2" + "${PROJECT_SOURCE_DIR}/src/util" + "${PROJECT_SOURCE_DIR}/include") + +# Collect sourcefiles +file(GLOB SRC_H + "${PROJECT_SOURCE_DIR}/include/git2.h" + "${PROJECT_SOURCE_DIR}/include/git2/*.h" + "${PROJECT_SOURCE_DIR}/include/git2/sys/*.h") +list(SORT SRC_H) +target_sources(libgit2 PRIVATE ${SRC_H}) + +file(GLOB SRC_GIT2 *.c *.h + streams/*.c streams/*.h + transports/*.c transports/*.h) +list(SORT SRC_GIT2) +target_sources(libgit2 PRIVATE ${SRC_GIT2}) + +if(WIN32 AND NOT CYGWIN) + # Add resource information on Windows + set(SRC_RC "git2.rc") +endif() + +if(APPLE) + # The old Secure Transport API has been deprecated in macOS 10.15. + set_source_files_properties(streams/stransport.c PROPERTIES COMPILE_FLAGS -Wno-deprecated) +endif() + +ide_split_sources(libgit2) +list(APPEND LIBGIT2_OBJECTS $ $ ${LIBGIT2_DEPENDENCY_OBJECTS}) +list(APPEND LIBGIT2_INCLUDES ${LIBGIT2_DEPENDENCY_INCLUDES}) + +target_include_directories(libgit2 PRIVATE ${LIBGIT2_INCLUDES} ${LIBGIT2_DEPENDENCY_INCLUDES} PUBLIC ${PROJECT_SOURCE_DIR}/include) +target_include_directories(libgit2 SYSTEM PRIVATE ${LIBGIT2_SYSTEM_INCLUDES}) + +set(LIBGIT2_INCLUDES ${LIBGIT2_INCLUDES} PARENT_SCOPE) +set(LIBGIT2_OBJECTS ${LIBGIT2_OBJECTS} PARENT_SCOPE) +set(LIBGIT2_DEPENDENCY_INCLUDES ${LIBGIT2_DEPENDENCY_INCLUDES} PARENT_SCOPE) +set(LIBGIT2_DEPENDENCY_OBJECTS ${LIBGIT2_DEPENDENCY_OBJECTS} PARENT_SCOPE) +set(LIBGIT2_SYSTEM_INCLUDES ${LIBGIT2_SYSTEM_INCLUDES} PARENT_SCOPE) +set(LIBGIT2_SYSTEM_LIBS ${LIBGIT2_SYSTEM_LIBS} PARENT_SCOPE) + +# +# Compile and link libgit2 +# + +add_library(libgit2package ${SRC_RC} ${LIBGIT2_OBJECTS}) +target_link_libraries(libgit2package ${LIBGIT2_SYSTEM_LIBS}) +target_include_directories(libgit2package SYSTEM PRIVATE ${LIBGIT2_INCLUDES}) + +set_target_properties(libgit2package PROPERTIES C_STANDARD 90) +set_target_properties(libgit2package PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) +set_target_properties(libgit2package PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) +set_target_properties(libgit2package PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) + +# Workaround for Cmake bug #0011240 (see http://public.kitware.com/Bug/view.php?id=11240) +# Win64+MSVC+static libs = linker error +if(MSVC AND GIT_ARCH_64 AND NOT BUILD_SHARED_LIBS) + set_target_properties(libgit2package PROPERTIES STATIC_LIBRARY_FLAGS "/MACHINE:x64") +endif() + +ide_split_sources(libgit2package) + +if(SONAME) + set_target_properties(libgit2package PROPERTIES VERSION ${libgit2_VERSION}) + set_target_properties(libgit2package PROPERTIES SOVERSION "${libgit2_VERSION_MAJOR}.${libgit2_VERSION_MINOR}") + if(LIBGIT2_FILENAME) + target_compile_definitions(libgit2package PRIVATE LIBGIT2_FILENAME=\"${LIBGIT2_FILENAME}\") + set_target_properties(libgit2package PROPERTIES OUTPUT_NAME ${LIBGIT2_FILENAME}) + elseif(DEFINED LIBGIT2_PREFIX) + set_target_properties(libgit2package PROPERTIES PREFIX "${LIBGIT2_PREFIX}") + endif() +endif() + +pkg_build_config(NAME "lib${LIBGIT2_FILENAME}" + VERSION ${libgit2_VERSION} + DESCRIPTION "The git library, take 2" + LIBS_SELF ${LIBGIT2_FILENAME} + PRIVATE_LIBS ${LIBGIT2_PC_LIBS} + REQUIRES ${LIBGIT2_PC_REQUIRES}) + +if(MSVC_IDE) + # Precompiled headers + set_target_properties(libgit2package PROPERTIES COMPILE_FLAGS "/Yuprecompiled.h /FIprecompiled.h") + set_source_files_properties(win32/precompiled.c COMPILE_FLAGS "/Ycprecompiled.h") +endif() + +# support experimental features and functionality + +configure_file(experimental.h.in "${PROJECT_BINARY_DIR}/include/git2/experimental.h") + +# translate filenames in the git2.h so that they match the install directory +# (allows for side-by-side installs of libgit2 and libgit2-experimental.) + +FILE(READ "${PROJECT_SOURCE_DIR}/include/git2.h" LIBGIT2_INCLUDE) +STRING(REGEX REPLACE "#include \"git2\/" "#include \"${LIBGIT2_FILENAME}/" LIBGIT2_INCLUDE "${LIBGIT2_INCLUDE}") +FILE(WRITE "${PROJECT_BINARY_DIR}/include/${LIBGIT2_FILENAME}.h" ${LIBGIT2_INCLUDE}) + +# Install + +install(TARGETS libgit2package + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) +install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/git2/ + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${LIBGIT2_FILENAME}") +install(FILES ${PROJECT_BINARY_DIR}/include/git2/experimental.h + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${LIBGIT2_FILENAME}") +install(FILES "${PROJECT_BINARY_DIR}/include/${LIBGIT2_FILENAME}.h" + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) diff --git a/src/libgit2/annotated_commit.c b/src/libgit2/annotated_commit.c new file mode 100644 index 0000000..c5c8ace --- /dev/null +++ b/src/libgit2/annotated_commit.c @@ -0,0 +1,240 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "annotated_commit.h" + +#include "refs.h" +#include "cache.h" + +#include "git2/commit.h" +#include "git2/refs.h" +#include "git2/repository.h" +#include "git2/annotated_commit.h" +#include "git2/revparse.h" +#include "git2/tree.h" +#include "git2/index.h" + +static int annotated_commit_init( + git_annotated_commit **out, + git_commit *commit, + const char *description) +{ + git_annotated_commit *annotated_commit; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(commit); + + *out = NULL; + + annotated_commit = git__calloc(1, sizeof(git_annotated_commit)); + GIT_ERROR_CHECK_ALLOC(annotated_commit); + + annotated_commit->type = GIT_ANNOTATED_COMMIT_REAL; + + if ((error = git_commit_dup(&annotated_commit->commit, commit)) < 0) + goto done; + + git_oid_tostr(annotated_commit->id_str, GIT_OID_MAX_HEXSIZE + 1, + git_commit_id(commit)); + + if (!description) + description = annotated_commit->id_str; + + annotated_commit->description = git__strdup(description); + GIT_ERROR_CHECK_ALLOC(annotated_commit->description); + +done: + if (!error) + *out = annotated_commit; + + return error; +} + +static int annotated_commit_init_from_id( + git_annotated_commit **out, + git_repository *repo, + const git_oid *id, + const char *description) +{ + git_commit *commit = NULL; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(id); + + *out = NULL; + + if ((error = git_commit_lookup(&commit, repo, id)) < 0) + goto done; + + error = annotated_commit_init(out, commit, description); + +done: + git_commit_free(commit); + return error; +} + +int git_annotated_commit_lookup( + git_annotated_commit **out, + git_repository *repo, + const git_oid *id) +{ + return annotated_commit_init_from_id(out, repo, id, NULL); +} + +int git_annotated_commit_from_commit( + git_annotated_commit **out, + git_commit *commit) +{ + return annotated_commit_init(out, commit, NULL); +} + +int git_annotated_commit_from_revspec( + git_annotated_commit **out, + git_repository *repo, + const char *revspec) +{ + git_object *obj, *commit; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(revspec); + + if ((error = git_revparse_single(&obj, repo, revspec)) < 0) + return error; + + if ((error = git_object_peel(&commit, obj, GIT_OBJECT_COMMIT))) { + git_object_free(obj); + return error; + } + + error = annotated_commit_init(out, (git_commit *)commit, revspec); + + git_object_free(obj); + git_object_free(commit); + + return error; +} + +int git_annotated_commit_from_ref( + git_annotated_commit **out, + git_repository *repo, + const git_reference *ref) +{ + git_object *peeled; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(ref); + + *out = NULL; + + if ((error = git_reference_peel(&peeled, ref, GIT_OBJECT_COMMIT)) < 0) + return error; + + error = annotated_commit_init_from_id(out, + repo, + git_object_id(peeled), + git_reference_name(ref)); + + if (!error) { + (*out)->ref_name = git__strdup(git_reference_name(ref)); + GIT_ERROR_CHECK_ALLOC((*out)->ref_name); + } + + git_object_free(peeled); + return error; +} + +int git_annotated_commit_from_head( + git_annotated_commit **out, + git_repository *repo) +{ + git_reference *head; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + *out = NULL; + + if ((error = git_reference_lookup(&head, repo, GIT_HEAD_FILE)) < 0) + return -1; + + error = git_annotated_commit_from_ref(out, repo, head); + + git_reference_free(head); + return error; +} + +int git_annotated_commit_from_fetchhead( + git_annotated_commit **out, + git_repository *repo, + const char *branch_name, + const char *remote_url, + const git_oid *id) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(branch_name); + GIT_ASSERT_ARG(remote_url); + GIT_ASSERT_ARG(id); + + if (annotated_commit_init_from_id(out, repo, id, branch_name) < 0) + return -1; + + (*out)->ref_name = git__strdup(branch_name); + GIT_ERROR_CHECK_ALLOC((*out)->ref_name); + + (*out)->remote_url = git__strdup(remote_url); + GIT_ERROR_CHECK_ALLOC((*out)->remote_url); + + return 0; +} + + +const git_oid *git_annotated_commit_id( + const git_annotated_commit *annotated_commit) +{ + GIT_ASSERT_ARG_WITH_RETVAL(annotated_commit, NULL); + return git_commit_id(annotated_commit->commit); +} + +const char *git_annotated_commit_ref( + const git_annotated_commit *annotated_commit) +{ + GIT_ASSERT_ARG_WITH_RETVAL(annotated_commit, NULL); + return annotated_commit->ref_name; +} + +void git_annotated_commit_free(git_annotated_commit *annotated_commit) +{ + if (annotated_commit == NULL) + return; + + switch (annotated_commit->type) { + case GIT_ANNOTATED_COMMIT_REAL: + git_commit_free(annotated_commit->commit); + git_tree_free(annotated_commit->tree); + git__free((char *)annotated_commit->description); + git__free((char *)annotated_commit->ref_name); + git__free((char *)annotated_commit->remote_url); + break; + case GIT_ANNOTATED_COMMIT_VIRTUAL: + git_index_free(annotated_commit->index); + git_array_clear(annotated_commit->parents); + break; + default: + abort(); + } + + git__free(annotated_commit); +} diff --git a/src/libgit2/annotated_commit.h b/src/libgit2/annotated_commit.h new file mode 100644 index 0000000..1f805fe --- /dev/null +++ b/src/libgit2/annotated_commit.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_annotated_commit_h__ +#define INCLUDE_annotated_commit_h__ + +#include "common.h" + +#include "oidarray.h" + +#include "git2/oid.h" + +typedef enum { + GIT_ANNOTATED_COMMIT_REAL = 1, + GIT_ANNOTATED_COMMIT_VIRTUAL = 2 +} git_annotated_commit_t; + +/** + * Internal structure for merge inputs. An annotated commit is generally + * "real" and backed by an actual commit in the repository, but merge will + * internally create "virtual" commits that are in-memory intermediate + * commits backed by an index. + */ +struct git_annotated_commit { + git_annotated_commit_t type; + + /* real commit */ + git_commit *commit; + git_tree *tree; + + /* virtual commit structure */ + git_index *index; + git_array_oid_t parents; + + /* how this commit was looked up */ + const char *description; + + const char *ref_name; + const char *remote_url; + + char id_str[GIT_OID_MAX_HEXSIZE + 1]; +}; + +extern int git_annotated_commit_from_head(git_annotated_commit **out, + git_repository *repo); +extern int git_annotated_commit_from_commit(git_annotated_commit **out, + git_commit *commit); + +#endif diff --git a/src/libgit2/apply.c b/src/libgit2/apply.c new file mode 100644 index 0000000..6b55b81 --- /dev/null +++ b/src/libgit2/apply.c @@ -0,0 +1,899 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2/apply.h" +#include "git2/patch.h" +#include "git2/filter.h" +#include "git2/blob.h" +#include "git2/index.h" +#include "git2/checkout.h" +#include "git2/repository.h" +#include "array.h" +#include "patch.h" +#include "futils.h" +#include "delta.h" +#include "zstream.h" +#include "reader.h" +#include "index.h" +#include "repository.h" +#include "apply.h" + +typedef struct { + /* The lines that we allocate ourself are allocated out of the pool. + * (Lines may have been allocated out of the diff.) + */ + git_pool pool; + git_vector lines; +} patch_image; + +static int apply_err(const char *fmt, ...) GIT_FORMAT_PRINTF(1, 2); +static int apply_err(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + git_error_vset(GIT_ERROR_PATCH, fmt, ap); + va_end(ap); + + return GIT_EAPPLYFAIL; +} + +static void patch_line_init( + git_diff_line *out, + const char *in, + size_t in_len, + size_t in_offset) +{ + out->content = in; + out->content_len = in_len; + out->content_offset = in_offset; +} + +#define PATCH_IMAGE_INIT { GIT_POOL_INIT, GIT_VECTOR_INIT } + +static int patch_image_init_fromstr( + patch_image *out, const char *in, size_t in_len) +{ + git_diff_line *line; + const char *start, *end; + + memset(out, 0x0, sizeof(patch_image)); + + if (git_pool_init(&out->pool, sizeof(git_diff_line)) < 0) + return -1; + + if (!in_len) + return 0; + + for (start = in; start < in + in_len; start = end) { + end = memchr(start, '\n', in_len - (start - in)); + + if (end == NULL) + end = in + in_len; + + else if (end < in + in_len) + end++; + + line = git_pool_mallocz(&out->pool, 1); + GIT_ERROR_CHECK_ALLOC(line); + + if (git_vector_insert(&out->lines, line) < 0) + return -1; + + patch_line_init(line, start, (end - start), (start - in)); + } + + return 0; +} + +static void patch_image_free(patch_image *image) +{ + if (image == NULL) + return; + + git_pool_clear(&image->pool); + git_vector_free(&image->lines); +} + +static bool match_hunk( + patch_image *image, + patch_image *preimage, + size_t linenum) +{ + bool match = 0; + size_t i; + + /* Ensure this hunk is within the image boundaries. */ + if (git_vector_length(&preimage->lines) + linenum > + git_vector_length(&image->lines)) + return 0; + + match = 1; + + /* Check exact match. */ + for (i = 0; i < git_vector_length(&preimage->lines); i++) { + git_diff_line *preimage_line = git_vector_get(&preimage->lines, i); + git_diff_line *image_line = git_vector_get(&image->lines, linenum + i); + + if (preimage_line->content_len != image_line->content_len || + memcmp(preimage_line->content, image_line->content, image_line->content_len) != 0) { + match = 0; + break; + } + } + + return match; +} + +static bool find_hunk_linenum( + size_t *out, + patch_image *image, + patch_image *preimage, + size_t linenum) +{ + size_t max = git_vector_length(&image->lines); + bool match; + + if (linenum > max) + linenum = max; + + match = match_hunk(image, preimage, linenum); + + *out = linenum; + return match; +} + +static int update_hunk( + patch_image *image, + size_t linenum, + patch_image *preimage, + patch_image *postimage) +{ + size_t postlen = git_vector_length(&postimage->lines); + size_t prelen = git_vector_length(&preimage->lines); + size_t i; + int error = 0; + + if (postlen > prelen) + error = git_vector_insert_null( + &image->lines, linenum, (postlen - prelen)); + else if (prelen > postlen) + error = git_vector_remove_range( + &image->lines, linenum, (prelen - postlen)); + + if (error) { + git_error_set_oom(); + return -1; + } + + for (i = 0; i < git_vector_length(&postimage->lines); i++) { + image->lines.contents[linenum + i] = + git_vector_get(&postimage->lines, i); + } + + return 0; +} + +typedef struct { + git_apply_options opts; + size_t skipped_new_lines; + size_t skipped_old_lines; +} apply_hunks_ctx; + +static int apply_hunk( + patch_image *image, + git_patch *patch, + git_patch_hunk *hunk, + apply_hunks_ctx *ctx) +{ + patch_image preimage = PATCH_IMAGE_INIT, postimage = PATCH_IMAGE_INIT; + size_t line_num, i; + int error = 0; + + if (ctx->opts.hunk_cb) { + error = ctx->opts.hunk_cb(&hunk->hunk, ctx->opts.payload); + + if (error) { + if (error > 0) { + ctx->skipped_new_lines += hunk->hunk.new_lines; + ctx->skipped_old_lines += hunk->hunk.old_lines; + error = 0; + } + + goto done; + } + } + + for (i = 0; i < hunk->line_count; i++) { + size_t linenum = hunk->line_start + i; + git_diff_line *line = git_array_get(patch->lines, linenum), *prev; + + if (!line) { + error = apply_err("preimage does not contain line %"PRIuZ, linenum); + goto done; + } + + switch (line->origin) { + case GIT_DIFF_LINE_CONTEXT_EOFNL: + case GIT_DIFF_LINE_DEL_EOFNL: + case GIT_DIFF_LINE_ADD_EOFNL: + prev = i ? git_array_get(patch->lines, linenum - 1) : NULL; + if (prev && prev->content[prev->content_len - 1] == '\n') + prev->content_len -= 1; + break; + case GIT_DIFF_LINE_CONTEXT: + if ((error = git_vector_insert(&preimage.lines, line)) < 0 || + (error = git_vector_insert(&postimage.lines, line)) < 0) + goto done; + break; + case GIT_DIFF_LINE_DELETION: + if ((error = git_vector_insert(&preimage.lines, line)) < 0) + goto done; + break; + case GIT_DIFF_LINE_ADDITION: + if ((error = git_vector_insert(&postimage.lines, line)) < 0) + goto done; + break; + } + } + + if (hunk->hunk.new_start) { + line_num = hunk->hunk.new_start - + ctx->skipped_new_lines + + ctx->skipped_old_lines - + 1; + } else { + line_num = 0; + } + + if (!find_hunk_linenum(&line_num, image, &preimage, line_num)) { + error = apply_err("hunk at line %d did not apply", + hunk->hunk.new_start); + goto done; + } + + error = update_hunk(image, line_num, &preimage, &postimage); + +done: + patch_image_free(&preimage); + patch_image_free(&postimage); + + return error; +} + +static int apply_hunks( + git_str *out, + const char *source, + size_t source_len, + git_patch *patch, + apply_hunks_ctx *ctx) +{ + git_patch_hunk *hunk; + git_diff_line *line; + patch_image image; + size_t i; + int error = 0; + + if ((error = patch_image_init_fromstr(&image, source, source_len)) < 0) + goto done; + + git_array_foreach(patch->hunks, i, hunk) { + if ((error = apply_hunk(&image, patch, hunk, ctx)) < 0) + goto done; + } + + git_vector_foreach(&image.lines, i, line) + git_str_put(out, line->content, line->content_len); + +done: + patch_image_free(&image); + + return error; +} + +static int apply_binary_delta( + git_str *out, + const char *source, + size_t source_len, + git_diff_binary_file *binary_file) +{ + git_str inflated = GIT_STR_INIT; + int error = 0; + + /* no diff means identical contents */ + if (binary_file->datalen == 0) + return git_str_put(out, source, source_len); + + error = git_zstream_inflatebuf(&inflated, + binary_file->data, binary_file->datalen); + + if (!error && inflated.size != binary_file->inflatedlen) { + error = apply_err("inflated delta does not match expected length"); + git_str_dispose(out); + } + + if (error < 0) + goto done; + + if (binary_file->type == GIT_DIFF_BINARY_DELTA) { + void *data; + size_t data_len; + + error = git_delta_apply(&data, &data_len, (void *)source, source_len, + (void *)inflated.ptr, inflated.size); + + out->ptr = data; + out->size = data_len; + out->asize = data_len; + } + else if (binary_file->type == GIT_DIFF_BINARY_LITERAL) { + git_str_swap(out, &inflated); + } + else { + error = apply_err("unknown binary delta type"); + goto done; + } + +done: + git_str_dispose(&inflated); + return error; +} + +static int apply_binary( + git_str *out, + const char *source, + size_t source_len, + git_patch *patch) +{ + git_str reverse = GIT_STR_INIT; + int error = 0; + + if (!patch->binary.contains_data) { + error = apply_err("patch does not contain binary data"); + goto done; + } + + if (!patch->binary.old_file.datalen && !patch->binary.new_file.datalen) + goto done; + + /* first, apply the new_file delta to the given source */ + if ((error = apply_binary_delta(out, source, source_len, + &patch->binary.new_file)) < 0) + goto done; + + /* second, apply the old_file delta to sanity check the result */ + if ((error = apply_binary_delta(&reverse, out->ptr, out->size, + &patch->binary.old_file)) < 0) + goto done; + + /* Verify that the resulting file with the reverse patch applied matches the source file */ + if (source_len != reverse.size || + (source_len && memcmp(source, reverse.ptr, source_len) != 0)) { + error = apply_err("binary patch did not apply cleanly"); + goto done; + } + +done: + if (error < 0) + git_str_dispose(out); + + git_str_dispose(&reverse); + return error; +} + +int git_apply__patch( + git_str *contents_out, + char **filename_out, + unsigned int *mode_out, + const char *source, + size_t source_len, + git_patch *patch, + const git_apply_options *given_opts) +{ + apply_hunks_ctx ctx = { GIT_APPLY_OPTIONS_INIT }; + char *filename = NULL; + unsigned int mode = 0; + int error = 0; + + GIT_ASSERT_ARG(contents_out); + GIT_ASSERT_ARG(filename_out); + GIT_ASSERT_ARG(mode_out); + GIT_ASSERT_ARG(source || !source_len); + GIT_ASSERT_ARG(patch); + + if (given_opts) + memcpy(&ctx.opts, given_opts, sizeof(git_apply_options)); + + *filename_out = NULL; + *mode_out = 0; + + if (patch->delta->status != GIT_DELTA_DELETED) { + const git_diff_file *newfile = &patch->delta->new_file; + + filename = git__strdup(newfile->path); + mode = newfile->mode ? + newfile->mode : GIT_FILEMODE_BLOB; + } + + if (patch->delta->flags & GIT_DIFF_FLAG_BINARY) + error = apply_binary(contents_out, source, source_len, patch); + else if (patch->hunks.size) + error = apply_hunks(contents_out, source, source_len, patch, &ctx); + else + error = git_str_put(contents_out, source, source_len); + + if (error) + goto done; + + if (patch->delta->status == GIT_DELTA_DELETED && + git_str_len(contents_out) > 0) { + error = apply_err("removal patch leaves file contents"); + goto done; + } + + *filename_out = filename; + *mode_out = mode; + +done: + if (error < 0) + git__free(filename); + + return error; +} + +static int apply_one( + git_repository *repo, + git_reader *preimage_reader, + git_index *preimage, + git_reader *postimage_reader, + git_index *postimage, + git_diff *diff, + git_strmap *removed_paths, + size_t i, + const git_apply_options *opts) +{ + git_patch *patch = NULL; + git_str pre_contents = GIT_STR_INIT, post_contents = GIT_STR_INIT; + const git_diff_delta *delta; + char *filename = NULL; + unsigned int mode; + git_oid pre_id, post_id; + git_filemode_t pre_filemode; + git_index_entry pre_entry, post_entry; + bool skip_preimage = false; + int error; + + if ((error = git_patch_from_diff(&patch, diff, i)) < 0) + goto done; + + delta = git_patch_get_delta(patch); + + if (opts->delta_cb) { + error = opts->delta_cb(delta, opts->payload); + + if (error) { + if (error > 0) + error = 0; + + goto done; + } + } + + /* + * Ensure that the file has not been deleted or renamed if we're + * applying a modification delta. + */ + if (delta->status != GIT_DELTA_RENAMED && + delta->status != GIT_DELTA_ADDED) { + if (git_strmap_exists(removed_paths, delta->old_file.path)) { + error = apply_err("path '%s' has been renamed or deleted", delta->old_file.path); + goto done; + } + } + + /* + * We may be applying a second delta to an already seen file. If so, + * use the already modified data in the postimage instead of the + * content from the index or working directory. (Don't do this in + * the case of a rename, which must be specified before additional + * deltas since we apply deltas to the target filename.) + */ + if (delta->status != GIT_DELTA_RENAMED) { + if ((error = git_reader_read(&pre_contents, &pre_id, &pre_filemode, + postimage_reader, delta->old_file.path)) == 0) { + skip_preimage = true; + } else if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = 0; + } else { + goto done; + } + } + + if (!skip_preimage && delta->status != GIT_DELTA_ADDED) { + error = git_reader_read(&pre_contents, &pre_id, &pre_filemode, + preimage_reader, delta->old_file.path); + + /* ENOTFOUND means the preimage was not found; apply failed. */ + if (error == GIT_ENOTFOUND) + error = GIT_EAPPLYFAIL; + + /* When applying to BOTH, the index did not match the workdir. */ + if (error == GIT_READER_MISMATCH) + error = apply_err("%s: does not match index", delta->old_file.path); + + if (error < 0) + goto done; + + /* + * We need to populate the preimage data structure with the + * contents that we are using as the preimage for this file. + * This allows us to apply patches to files that have been + * modified in the working directory. During checkout, + * we will use this expected preimage as the baseline, and + * limit checkout to only the paths affected by patch + * application. (Without this, we would fail to write the + * postimage contents to any file that had been modified + * from HEAD on-disk, even if the patch application succeeded.) + * Use the contents from the delta where available - some + * fields may not be available, like the old file mode (eg in + * an exact rename situation) so trust the patch parsing to + * validate and use the preimage data in that case. + */ + if (preimage) { + memset(&pre_entry, 0, sizeof(git_index_entry)); + pre_entry.path = delta->old_file.path; + pre_entry.mode = delta->old_file.mode ? delta->old_file.mode : pre_filemode; + git_oid_cpy(&pre_entry.id, &pre_id); + + if ((error = git_index_add(preimage, &pre_entry)) < 0) + goto done; + } + } + + if (delta->status != GIT_DELTA_DELETED) { + if ((error = git_apply__patch(&post_contents, &filename, &mode, + pre_contents.ptr, pre_contents.size, patch, opts)) < 0 || + (error = git_blob_create_from_buffer(&post_id, repo, + post_contents.ptr, post_contents.size)) < 0) + goto done; + + memset(&post_entry, 0, sizeof(git_index_entry)); + post_entry.path = filename; + post_entry.mode = mode; + git_oid_cpy(&post_entry.id, &post_id); + + if ((error = git_index_add(postimage, &post_entry)) < 0) + goto done; + } + + if (delta->status == GIT_DELTA_RENAMED || + delta->status == GIT_DELTA_DELETED) + error = git_strmap_set(removed_paths, delta->old_file.path, (char *) delta->old_file.path); + + if (delta->status == GIT_DELTA_RENAMED || + delta->status == GIT_DELTA_ADDED) + git_strmap_delete(removed_paths, delta->new_file.path); + +done: + git_str_dispose(&pre_contents); + git_str_dispose(&post_contents); + git__free(filename); + git_patch_free(patch); + + return error; +} + +static int apply_deltas( + git_repository *repo, + git_reader *pre_reader, + git_index *preimage, + git_reader *post_reader, + git_index *postimage, + git_diff *diff, + const git_apply_options *opts) +{ + git_strmap *removed_paths; + size_t i; + int error = 0; + + if (git_strmap_new(&removed_paths) < 0) + return -1; + + for (i = 0; i < git_diff_num_deltas(diff); i++) { + if ((error = apply_one(repo, pre_reader, preimage, post_reader, postimage, diff, removed_paths, i, opts)) < 0) + goto done; + } + +done: + git_strmap_free(removed_paths); + return error; +} + +int git_apply_to_tree( + git_index **out, + git_repository *repo, + git_tree *preimage, + git_diff *diff, + const git_apply_options *given_opts) +{ + git_index *postimage = NULL; + git_reader *pre_reader = NULL, *post_reader = NULL; + git_apply_options opts = GIT_APPLY_OPTIONS_INIT; + const git_diff_delta *delta; + size_t i; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(preimage); + GIT_ASSERT_ARG(diff); + + *out = NULL; + + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_apply_options)); + + if ((error = git_reader_for_tree(&pre_reader, preimage)) < 0) + goto done; + + /* + * put the current tree into the postimage as-is - the diff will + * replace any entries contained therein + */ + if ((error = git_index__new(&postimage, repo->oid_type)) < 0 || + (error = git_index_read_tree(postimage, preimage)) < 0 || + (error = git_reader_for_index(&post_reader, repo, postimage)) < 0) + goto done; + + /* + * Remove the old paths from the index before applying diffs - + * we need to do a full pass to remove them before adding deltas, + * in order to handle rename situations. + */ + for (i = 0; i < git_diff_num_deltas(diff); i++) { + delta = git_diff_get_delta(diff, i); + + if (delta->status == GIT_DELTA_DELETED || + delta->status == GIT_DELTA_RENAMED) { + if ((error = git_index_remove(postimage, + delta->old_file.path, 0)) < 0) + goto done; + } + } + + if ((error = apply_deltas(repo, pre_reader, NULL, post_reader, postimage, diff, &opts)) < 0) + goto done; + + *out = postimage; + +done: + if (error < 0) + git_index_free(postimage); + + git_reader_free(pre_reader); + git_reader_free(post_reader); + + return error; +} + +static int git_apply__to_workdir( + git_repository *repo, + git_diff *diff, + git_index *preimage, + git_index *postimage, + git_apply_location_t location, + git_apply_options *opts) +{ + git_vector paths = GIT_VECTOR_INIT; + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + const git_diff_delta *delta; + size_t i; + int error; + + GIT_UNUSED(opts); + + /* + * Limit checkout to the paths affected by the diff; this ensures + * that other modifications in the working directory are unaffected. + */ + if ((error = git_vector_init(&paths, git_diff_num_deltas(diff), NULL)) < 0) + goto done; + + for (i = 0; i < git_diff_num_deltas(diff); i++) { + delta = git_diff_get_delta(diff, i); + + if ((error = git_vector_insert(&paths, (void *)delta->old_file.path)) < 0) + goto done; + + if (strcmp(delta->old_file.path, delta->new_file.path) && + (error = git_vector_insert(&paths, (void *)delta->new_file.path)) < 0) + goto done; + } + + checkout_opts.checkout_strategy |= GIT_CHECKOUT_SAFE; + checkout_opts.checkout_strategy |= GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH; + checkout_opts.checkout_strategy |= GIT_CHECKOUT_DONT_WRITE_INDEX; + + if (location == GIT_APPLY_LOCATION_WORKDIR) + checkout_opts.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; + + checkout_opts.paths.strings = (char **)paths.contents; + checkout_opts.paths.count = paths.length; + + checkout_opts.baseline_index = preimage; + + error = git_checkout_index(repo, postimage, &checkout_opts); + +done: + git_vector_free(&paths); + return error; +} + +static int git_apply__to_index( + git_repository *repo, + git_diff *diff, + git_index *preimage, + git_index *postimage, + git_apply_options *opts) +{ + git_index *index = NULL; + const git_diff_delta *delta; + const git_index_entry *entry; + size_t i; + int error; + + GIT_UNUSED(preimage); + GIT_UNUSED(opts); + + if ((error = git_repository_index(&index, repo)) < 0) + goto done; + + /* Remove deleted (or renamed) paths from the index. */ + for (i = 0; i < git_diff_num_deltas(diff); i++) { + delta = git_diff_get_delta(diff, i); + + if (delta->status == GIT_DELTA_DELETED || + delta->status == GIT_DELTA_RENAMED) { + if ((error = git_index_remove(index, delta->old_file.path, 0)) < 0) + goto done; + } + } + + /* Then add the changes back to the index. */ + for (i = 0; i < git_index_entrycount(postimage); i++) { + entry = git_index_get_byindex(postimage, i); + + if ((error = git_index_add(index, entry)) < 0) + goto done; + } + +done: + git_index_free(index); + return error; +} + +int git_apply_options_init(git_apply_options *opts, unsigned int version) +{ + GIT_ASSERT_ARG(opts); + + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_apply_options, GIT_APPLY_OPTIONS_INIT); + return 0; +} + +/* + * Handle the three application options ("locations"): + * + * GIT_APPLY_LOCATION_WORKDIR: the default, emulates `git apply`. + * Applies the diff only to the workdir items and ignores the index + * entirely. + * + * GIT_APPLY_LOCATION_INDEX: emulates `git apply --cached`. + * Applies the diff only to the index items and ignores the workdir + * completely. + * + * GIT_APPLY_LOCATION_BOTH: emulates `git apply --index`. + * Applies the diff to both the index items and the working directory + * items. + */ + +int git_apply( + git_repository *repo, + git_diff *diff, + git_apply_location_t location, + const git_apply_options *given_opts) +{ + git_indexwriter indexwriter = GIT_INDEXWRITER_INIT; + git_index *index = NULL, *preimage = NULL, *postimage = NULL; + git_reader *pre_reader = NULL, *post_reader = NULL; + git_apply_options opts = GIT_APPLY_OPTIONS_INIT; + int error = GIT_EINVALID; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(diff); + + GIT_ERROR_CHECK_VERSION( + given_opts, GIT_APPLY_OPTIONS_VERSION, "git_apply_options"); + + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_apply_options)); + + /* + * by default, we apply a patch directly to the working directory; + * in `--cached` or `--index` mode, we apply to the contents already + * in the index. + */ + switch (location) { + case GIT_APPLY_LOCATION_BOTH: + error = git_reader_for_workdir(&pre_reader, repo, true); + break; + case GIT_APPLY_LOCATION_INDEX: + error = git_reader_for_index(&pre_reader, repo, NULL); + break; + case GIT_APPLY_LOCATION_WORKDIR: + error = git_reader_for_workdir(&pre_reader, repo, false); + break; + default: + GIT_ASSERT(false); + } + + if (error < 0) + goto done; + + /* + * Build the preimage and postimage (differences). Note that + * this is not the complete preimage or postimage, it only + * contains the files affected by the patch. We want to avoid + * having the full repo index, so we will limit our checkout + * to only write these files that were affected by the diff. + */ + if ((error = git_index__new(&preimage, repo->oid_type)) < 0 || + (error = git_index__new(&postimage, repo->oid_type)) < 0 || + (error = git_reader_for_index(&post_reader, repo, postimage)) < 0) + goto done; + + if (!(opts.flags & GIT_APPLY_CHECK)) + if ((error = git_repository_index(&index, repo)) < 0 || + (error = git_indexwriter_init(&indexwriter, index)) < 0) + goto done; + + if ((error = apply_deltas(repo, pre_reader, preimage, post_reader, postimage, diff, &opts)) < 0) + goto done; + + if ((opts.flags & GIT_APPLY_CHECK)) + goto done; + + switch (location) { + case GIT_APPLY_LOCATION_BOTH: + error = git_apply__to_workdir(repo, diff, preimage, postimage, location, &opts); + break; + case GIT_APPLY_LOCATION_INDEX: + error = git_apply__to_index(repo, diff, preimage, postimage, &opts); + break; + case GIT_APPLY_LOCATION_WORKDIR: + error = git_apply__to_workdir(repo, diff, preimage, postimage, location, &opts); + break; + default: + GIT_ASSERT(false); + } + + if (error < 0) + goto done; + + error = git_indexwriter_commit(&indexwriter); + +done: + git_indexwriter_cleanup(&indexwriter); + git_index_free(postimage); + git_index_free(preimage); + git_index_free(index); + git_reader_free(pre_reader); + git_reader_free(post_reader); + + return error; +} diff --git a/src/libgit2/apply.h b/src/libgit2/apply.h new file mode 100644 index 0000000..e990a71 --- /dev/null +++ b/src/libgit2/apply.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_apply_h__ +#define INCLUDE_apply_h__ + +#include "common.h" + +#include "git2/patch.h" +#include "git2/apply.h" +#include "str.h" + +extern int git_apply__patch( + git_str *out, + char **filename, + unsigned int *mode, + const char *source, + size_t source_len, + git_patch *patch, + const git_apply_options *opts); + +#endif diff --git a/src/libgit2/attr.c b/src/libgit2/attr.c new file mode 100644 index 0000000..1623b1d --- /dev/null +++ b/src/libgit2/attr.c @@ -0,0 +1,700 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "attr.h" + +#include "repository.h" +#include "sysdir.h" +#include "config.h" +#include "attr_file.h" +#include "ignore.h" +#include "git2/oid.h" +#include + +const char *git_attr__true = "[internal]__TRUE__"; +const char *git_attr__false = "[internal]__FALSE__"; +const char *git_attr__unset = "[internal]__UNSET__"; + +git_attr_value_t git_attr_value(const char *attr) +{ + if (attr == NULL || attr == git_attr__unset) + return GIT_ATTR_VALUE_UNSPECIFIED; + + if (attr == git_attr__true) + return GIT_ATTR_VALUE_TRUE; + + if (attr == git_attr__false) + return GIT_ATTR_VALUE_FALSE; + + return GIT_ATTR_VALUE_STRING; +} + +static int collect_attr_files( + git_repository *repo, + git_attr_session *attr_session, + git_attr_options *opts, + const char *path, + git_vector *files); + +static void release_attr_files(git_vector *files); + +int git_attr_get_ext( + const char **value, + git_repository *repo, + git_attr_options *opts, + const char *pathname, + const char *name) +{ + int error; + git_attr_path path; + git_vector files = GIT_VECTOR_INIT; + size_t i, j; + git_attr_file *file; + git_attr_name attr; + git_attr_rule *rule; + git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN; + + GIT_ASSERT_ARG(value); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + GIT_ERROR_CHECK_VERSION(opts, GIT_ATTR_OPTIONS_VERSION, "git_attr_options"); + + *value = NULL; + + if (git_repository_is_bare(repo)) + dir_flag = GIT_DIR_FLAG_FALSE; + + if (git_attr_path__init(&path, pathname, git_repository_workdir(repo), dir_flag) < 0) + return -1; + + if ((error = collect_attr_files(repo, NULL, opts, pathname, &files)) < 0) + goto cleanup; + + memset(&attr, 0, sizeof(attr)); + attr.name = name; + attr.name_hash = git_attr_file__name_hash(name); + + git_vector_foreach(&files, i, file) { + + git_attr_file__foreach_matching_rule(file, &path, j, rule) { + size_t pos; + + if (!git_vector_bsearch(&pos, &rule->assigns, &attr)) { + *value = ((git_attr_assignment *)git_vector_get( + &rule->assigns, pos))->value; + goto cleanup; + } + } + } + +cleanup: + release_attr_files(&files); + git_attr_path__free(&path); + + return error; +} + +int git_attr_get( + const char **value, + git_repository *repo, + uint32_t flags, + const char *pathname, + const char *name) +{ + git_attr_options opts = GIT_ATTR_OPTIONS_INIT; + + opts.flags = flags; + + return git_attr_get_ext(value, repo, &opts, pathname, name); +} + + +typedef struct { + git_attr_name name; + git_attr_assignment *found; +} attr_get_many_info; + +int git_attr_get_many_with_session( + const char **values, + git_repository *repo, + git_attr_session *attr_session, + git_attr_options *opts, + const char *pathname, + size_t num_attr, + const char **names) +{ + int error; + git_attr_path path; + git_vector files = GIT_VECTOR_INIT; + size_t i, j, k; + git_attr_file *file; + git_attr_rule *rule; + attr_get_many_info *info = NULL; + size_t num_found = 0; + git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN; + + if (!num_attr) + return 0; + + GIT_ASSERT_ARG(values); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(pathname); + GIT_ASSERT_ARG(names); + GIT_ERROR_CHECK_VERSION(opts, GIT_ATTR_OPTIONS_VERSION, "git_attr_options"); + + if (git_repository_is_bare(repo)) + dir_flag = GIT_DIR_FLAG_FALSE; + + if (git_attr_path__init(&path, pathname, git_repository_workdir(repo), dir_flag) < 0) + return -1; + + if ((error = collect_attr_files(repo, attr_session, opts, pathname, &files)) < 0) + goto cleanup; + + info = git__calloc(num_attr, sizeof(attr_get_many_info)); + GIT_ERROR_CHECK_ALLOC(info); + + git_vector_foreach(&files, i, file) { + + git_attr_file__foreach_matching_rule(file, &path, j, rule) { + + for (k = 0; k < num_attr; k++) { + size_t pos; + + if (info[k].found != NULL) /* already found assignment */ + continue; + + if (!info[k].name.name) { + info[k].name.name = names[k]; + info[k].name.name_hash = git_attr_file__name_hash(names[k]); + } + + if (!git_vector_bsearch(&pos, &rule->assigns, &info[k].name)) { + info[k].found = (git_attr_assignment *) + git_vector_get(&rule->assigns, pos); + values[k] = info[k].found->value; + + if (++num_found == num_attr) + goto cleanup; + } + } + } + } + + for (k = 0; k < num_attr; k++) { + if (!info[k].found) + values[k] = NULL; + } + +cleanup: + release_attr_files(&files); + git_attr_path__free(&path); + git__free(info); + + return error; +} + +int git_attr_get_many( + const char **values, + git_repository *repo, + uint32_t flags, + const char *pathname, + size_t num_attr, + const char **names) +{ + git_attr_options opts = GIT_ATTR_OPTIONS_INIT; + + opts.flags = flags; + + return git_attr_get_many_with_session( + values, repo, NULL, &opts, pathname, num_attr, names); +} + +int git_attr_get_many_ext( + const char **values, + git_repository *repo, + git_attr_options *opts, + const char *pathname, + size_t num_attr, + const char **names) +{ + return git_attr_get_many_with_session( + values, repo, NULL, opts, pathname, num_attr, names); +} + +int git_attr_foreach( + git_repository *repo, + uint32_t flags, + const char *pathname, + int (*callback)(const char *name, const char *value, void *payload), + void *payload) +{ + git_attr_options opts = GIT_ATTR_OPTIONS_INIT; + + opts.flags = flags; + + return git_attr_foreach_ext(repo, &opts, pathname, callback, payload); +} + +int git_attr_foreach_ext( + git_repository *repo, + git_attr_options *opts, + const char *pathname, + int (*callback)(const char *name, const char *value, void *payload), + void *payload) +{ + int error; + git_attr_path path; + git_vector files = GIT_VECTOR_INIT; + size_t i, j, k; + git_attr_file *file; + git_attr_rule *rule; + git_attr_assignment *assign; + git_strmap *seen = NULL; + git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(callback); + GIT_ERROR_CHECK_VERSION(opts, GIT_ATTR_OPTIONS_VERSION, "git_attr_options"); + + if (git_repository_is_bare(repo)) + dir_flag = GIT_DIR_FLAG_FALSE; + + if (git_attr_path__init(&path, pathname, git_repository_workdir(repo), dir_flag) < 0) + return -1; + + if ((error = collect_attr_files(repo, NULL, opts, pathname, &files)) < 0 || + (error = git_strmap_new(&seen)) < 0) + goto cleanup; + + git_vector_foreach(&files, i, file) { + + git_attr_file__foreach_matching_rule(file, &path, j, rule) { + + git_vector_foreach(&rule->assigns, k, assign) { + /* skip if higher priority assignment was already seen */ + if (git_strmap_exists(seen, assign->name)) + continue; + + if ((error = git_strmap_set(seen, assign->name, assign)) < 0) + goto cleanup; + + error = callback(assign->name, assign->value, payload); + if (error) { + git_error_set_after_callback(error); + goto cleanup; + } + } + } + } + +cleanup: + git_strmap_free(seen); + release_attr_files(&files); + git_attr_path__free(&path); + + return error; +} + +static int preload_attr_source( + git_repository *repo, + git_attr_session *attr_session, + git_attr_file_source *source) +{ + int error; + git_attr_file *preload = NULL; + + if (!source) + return 0; + + error = git_attr_cache__get(&preload, repo, attr_session, source, + git_attr_file__parse_buffer, true); + + if (!error) + git_attr_file__free(preload); + + return error; +} + +GIT_INLINE(int) preload_attr_file( + git_repository *repo, + git_attr_session *attr_session, + const char *base, + const char *filename) +{ + git_attr_file_source source = { GIT_ATTR_FILE_SOURCE_FILE }; + + if (!filename) + return 0; + + source.base = base; + source.filename = filename; + + return preload_attr_source(repo, attr_session, &source); +} + +static int system_attr_file( + git_str *out, + git_attr_session *attr_session) +{ + int error; + + if (!attr_session) { + error = git_sysdir_find_system_file(out, GIT_ATTR_FILE_SYSTEM); + + if (error == GIT_ENOTFOUND) + git_error_clear(); + + return error; + } + + if (!attr_session->init_sysdir) { + error = git_sysdir_find_system_file(&attr_session->sysdir, GIT_ATTR_FILE_SYSTEM); + + if (error == GIT_ENOTFOUND) + git_error_clear(); + else if (error) + return error; + + attr_session->init_sysdir = 1; + } + + if (attr_session->sysdir.size == 0) + return GIT_ENOTFOUND; + + /* We can safely provide a git_str with no allocation (asize == 0) to + * a consumer. This allows them to treat this as a regular `git_str`, + * but their call to `git_str_dispose` will not attempt to free it. + */ + git_str_attach_notowned( + out, attr_session->sysdir.ptr, attr_session->sysdir.size); + return 0; +} + +static int attr_setup( + git_repository *repo, + git_attr_session *attr_session, + git_attr_options *opts) +{ + git_str system = GIT_STR_INIT, info = GIT_STR_INIT; + git_attr_file_source index_source = { GIT_ATTR_FILE_SOURCE_INDEX, NULL, GIT_ATTR_FILE, NULL }; + git_attr_file_source head_source = { GIT_ATTR_FILE_SOURCE_HEAD, NULL, GIT_ATTR_FILE, NULL }; + git_attr_file_source commit_source = { GIT_ATTR_FILE_SOURCE_COMMIT, NULL, GIT_ATTR_FILE, NULL }; + git_index *idx = NULL; + const char *workdir; + int error = 0; + + if (attr_session && attr_session->init_setup) + return 0; + + if ((error = git_attr_cache__init(repo)) < 0) + return error; + + /* + * Preload attribute files that could contain macros so the + * definitions will be available for later file parsing. + */ + + if ((error = system_attr_file(&system, attr_session)) < 0 || + (error = preload_attr_file(repo, attr_session, NULL, system.ptr)) < 0) { + if (error != GIT_ENOTFOUND) + goto out; + + error = 0; + } + + if ((error = preload_attr_file(repo, attr_session, NULL, + git_repository_attr_cache(repo)->cfg_attr_file)) < 0) + goto out; + + if ((error = git_repository__item_path(&info, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 || + (error = preload_attr_file(repo, attr_session, info.ptr, GIT_ATTR_FILE_INREPO)) < 0) { + if (error != GIT_ENOTFOUND) + goto out; + + error = 0; + } + + if ((workdir = git_repository_workdir(repo)) != NULL && + (error = preload_attr_file(repo, attr_session, workdir, GIT_ATTR_FILE)) < 0) + goto out; + + if ((error = git_repository_index__weakptr(&idx, repo)) < 0 || + (error = preload_attr_source(repo, attr_session, &index_source)) < 0) + goto out; + + if ((opts && (opts->flags & GIT_ATTR_CHECK_INCLUDE_HEAD) != 0) && + (error = preload_attr_source(repo, attr_session, &head_source)) < 0) + goto out; + + if ((opts && (opts->flags & GIT_ATTR_CHECK_INCLUDE_COMMIT) != 0)) { +#ifndef GIT_DEPRECATE_HARD + if (opts->commit_id) + commit_source.commit_id = opts->commit_id; + else +#endif + commit_source.commit_id = &opts->attr_commit_id; + + if ((error = preload_attr_source(repo, attr_session, &commit_source)) < 0) + goto out; + } + + if (attr_session) + attr_session->init_setup = 1; + +out: + git_str_dispose(&system); + git_str_dispose(&info); + + return error; +} + +int git_attr_add_macro( + git_repository *repo, + const char *name, + const char *values) +{ + int error; + git_attr_rule *macro = NULL; + git_pool *pool; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + if ((error = git_attr_cache__init(repo)) < 0) + return error; + + macro = git__calloc(1, sizeof(git_attr_rule)); + GIT_ERROR_CHECK_ALLOC(macro); + + pool = &git_repository_attr_cache(repo)->pool; + + macro->match.pattern = git_pool_strdup(pool, name); + GIT_ERROR_CHECK_ALLOC(macro->match.pattern); + + macro->match.length = strlen(macro->match.pattern); + macro->match.flags = GIT_ATTR_FNMATCH_MACRO; + + error = git_attr_assignment__parse(repo, pool, ¯o->assigns, &values); + + if (!error) + error = git_attr_cache__insert_macro(repo, macro); + + if (error < 0) + git_attr_rule__free(macro); + + return error; +} + +typedef struct { + git_repository *repo; + git_attr_session *attr_session; + git_attr_options *opts; + const char *workdir; + git_index *index; + git_vector *files; +} attr_walk_up_info; + +static int attr_decide_sources( + uint32_t flags, + bool has_wd, + bool has_index, + git_attr_file_source_t *srcs) +{ + int count = 0; + + switch (flags & 0x03) { + case GIT_ATTR_CHECK_FILE_THEN_INDEX: + if (has_wd) + srcs[count++] = GIT_ATTR_FILE_SOURCE_FILE; + if (has_index) + srcs[count++] = GIT_ATTR_FILE_SOURCE_INDEX; + break; + case GIT_ATTR_CHECK_INDEX_THEN_FILE: + if (has_index) + srcs[count++] = GIT_ATTR_FILE_SOURCE_INDEX; + if (has_wd) + srcs[count++] = GIT_ATTR_FILE_SOURCE_FILE; + break; + case GIT_ATTR_CHECK_INDEX_ONLY: + if (has_index) + srcs[count++] = GIT_ATTR_FILE_SOURCE_INDEX; + break; + } + + if ((flags & GIT_ATTR_CHECK_INCLUDE_HEAD) != 0) + srcs[count++] = GIT_ATTR_FILE_SOURCE_HEAD; + + if ((flags & GIT_ATTR_CHECK_INCLUDE_COMMIT) != 0) + srcs[count++] = GIT_ATTR_FILE_SOURCE_COMMIT; + + return count; +} + +static int push_attr_source( + git_repository *repo, + git_attr_session *attr_session, + git_vector *list, + git_attr_file_source *source, + bool allow_macros) +{ + int error = 0; + git_attr_file *file = NULL; + + error = git_attr_cache__get(&file, repo, attr_session, + source, + git_attr_file__parse_buffer, + allow_macros); + + if (error < 0) + return error; + + if (file != NULL) { + if ((error = git_vector_insert(list, file)) < 0) + git_attr_file__free(file); + } + + return error; +} + +GIT_INLINE(int) push_attr_file( + git_repository *repo, + git_attr_session *attr_session, + git_vector *list, + const char *base, + const char *filename) +{ + git_attr_file_source source = { GIT_ATTR_FILE_SOURCE_FILE, base, filename }; + return push_attr_source(repo, attr_session, list, &source, true); +} + +static int push_one_attr(void *ref, const char *path) +{ + attr_walk_up_info *info = (attr_walk_up_info *)ref; + git_attr_file_source_t src[GIT_ATTR_FILE_NUM_SOURCES]; + int error = 0, n_src, i; + bool allow_macros; + + n_src = attr_decide_sources(info->opts ? info->opts->flags : 0, + info->workdir != NULL, + info->index != NULL, + src); + + allow_macros = info->workdir ? !strcmp(info->workdir, path) : false; + + for (i = 0; !error && i < n_src; ++i) { + git_attr_file_source source = { src[i], path, GIT_ATTR_FILE }; + + if (src[i] == GIT_ATTR_FILE_SOURCE_COMMIT && info->opts) { +#ifndef GIT_DEPRECATE_HARD + if (info->opts->commit_id) + source.commit_id = info->opts->commit_id; + else +#endif + source.commit_id = &info->opts->attr_commit_id; + } + + error = push_attr_source(info->repo, info->attr_session, info->files, + &source, allow_macros); + } + + return error; +} + +static void release_attr_files(git_vector *files) +{ + size_t i; + git_attr_file *file; + + git_vector_foreach(files, i, file) { + git_attr_file__free(file); + files->contents[i] = NULL; + } + git_vector_free(files); +} + +static int collect_attr_files( + git_repository *repo, + git_attr_session *attr_session, + git_attr_options *opts, + const char *path, + git_vector *files) +{ + int error = 0; + git_str dir = GIT_STR_INIT, attrfile = GIT_STR_INIT; + const char *workdir = git_repository_workdir(repo); + attr_walk_up_info info = { NULL }; + + GIT_ASSERT(!git_fs_path_is_absolute(path)); + + if ((error = attr_setup(repo, attr_session, opts)) < 0) + return error; + + /* Resolve path in a non-bare repo */ + if (workdir != NULL) { + if (!(error = git_repository_workdir_path(&dir, repo, path))) + error = git_fs_path_find_dir(&dir); + } + else { + error = git_fs_path_dirname_r(&dir, path); + } + + if (error < 0) + goto cleanup; + + /* in precedence order highest to lowest: + * - $GIT_DIR/info/attributes + * - path components with .gitattributes + * - config core.attributesfile + * - $GIT_PREFIX/etc/gitattributes + */ + + if ((error = git_repository__item_path(&attrfile, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 || + (error = push_attr_file(repo, attr_session, files, attrfile.ptr, GIT_ATTR_FILE_INREPO)) < 0) { + if (error != GIT_ENOTFOUND) + goto cleanup; + } + + info.repo = repo; + info.attr_session = attr_session; + info.opts = opts; + info.workdir = workdir; + if (git_repository_index__weakptr(&info.index, repo) < 0) + git_error_clear(); /* no error even if there is no index */ + info.files = files; + + if (!strcmp(dir.ptr, ".")) + error = push_one_attr(&info, ""); + else + error = git_fs_path_walk_up(&dir, workdir, push_one_attr, &info); + + if (error < 0) + goto cleanup; + + if (git_repository_attr_cache(repo)->cfg_attr_file != NULL) { + error = push_attr_file(repo, attr_session, files, NULL, git_repository_attr_cache(repo)->cfg_attr_file); + if (error < 0) + goto cleanup; + } + + if (!opts || (opts->flags & GIT_ATTR_CHECK_NO_SYSTEM) == 0) { + error = system_attr_file(&dir, attr_session); + + if (!error) + error = push_attr_file(repo, attr_session, files, NULL, dir.ptr); + else if (error == GIT_ENOTFOUND) + error = 0; + } + + cleanup: + if (error < 0) + release_attr_files(files); + git_str_dispose(&attrfile); + git_str_dispose(&dir); + + return error; +} diff --git a/src/libgit2/attr.h b/src/libgit2/attr.h new file mode 100644 index 0000000..9775652 --- /dev/null +++ b/src/libgit2/attr.h @@ -0,0 +1,15 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_attr_h__ +#define INCLUDE_attr_h__ + +#include "common.h" + +#include "attr_file.h" +#include "attrcache.h" + +#endif diff --git a/src/libgit2/attr_file.c b/src/libgit2/attr_file.c new file mode 100644 index 0000000..afa8ec7 --- /dev/null +++ b/src/libgit2/attr_file.c @@ -0,0 +1,1027 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "attr_file.h" + +#include "repository.h" +#include "filebuf.h" +#include "attrcache.h" +#include "git2/blob.h" +#include "git2/tree.h" +#include "blob.h" +#include "index.h" +#include "wildmatch.h" +#include + +static void attr_file_free(git_attr_file *file) +{ + bool unlock = !git_mutex_lock(&file->lock); + git_attr_file__clear_rules(file, false); + git_pool_clear(&file->pool); + if (unlock) + git_mutex_unlock(&file->lock); + git_mutex_free(&file->lock); + + git__memzero(file, sizeof(*file)); + git__free(file); +} + +int git_attr_file__new( + git_attr_file **out, + git_attr_file_entry *entry, + git_attr_file_source *source) +{ + git_attr_file *attrs = git__calloc(1, sizeof(git_attr_file)); + GIT_ERROR_CHECK_ALLOC(attrs); + + if (git_mutex_init(&attrs->lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to initialize lock"); + goto on_error; + } + + if (git_pool_init(&attrs->pool, 1) < 0) + goto on_error; + + GIT_REFCOUNT_INC(attrs); + attrs->entry = entry; + memcpy(&attrs->source, source, sizeof(git_attr_file_source)); + *out = attrs; + return 0; + +on_error: + git__free(attrs); + return -1; +} + +int git_attr_file__clear_rules(git_attr_file *file, bool need_lock) +{ + unsigned int i; + git_attr_rule *rule; + + if (need_lock && git_mutex_lock(&file->lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock attribute file"); + return -1; + } + + git_vector_foreach(&file->rules, i, rule) + git_attr_rule__free(rule); + git_vector_free(&file->rules); + + if (need_lock) + git_mutex_unlock(&file->lock); + + return 0; +} + +void git_attr_file__free(git_attr_file *file) +{ + if (!file) + return; + GIT_REFCOUNT_DEC(file, attr_file_free); +} + +static int attr_file_oid_from_index( + git_oid *oid, git_repository *repo, const char *path) +{ + int error; + git_index *idx; + size_t pos; + const git_index_entry *entry; + + if ((error = git_repository_index__weakptr(&idx, repo)) < 0 || + (error = git_index__find_pos(&pos, idx, path, 0, 0)) < 0) + return error; + + if (!(entry = git_index_get_byindex(idx, pos))) + return GIT_ENOTFOUND; + + *oid = entry->id; + return 0; +} + +int git_attr_file__load( + git_attr_file **out, + git_repository *repo, + git_attr_session *attr_session, + git_attr_file_entry *entry, + git_attr_file_source *source, + git_attr_file_parser parser, + bool allow_macros) +{ + int error = 0; + git_commit *commit = NULL; + git_tree *tree = NULL; + git_tree_entry *tree_entry = NULL; + git_blob *blob = NULL; + git_str content = GIT_STR_INIT; + const char *content_str; + git_attr_file *file; + struct stat st; + bool nonexistent = false; + int bom_offset; + git_str_bom_t bom; + git_oid id; + git_object_size_t blobsize; + + *out = NULL; + + switch (source->type) { + case GIT_ATTR_FILE_SOURCE_MEMORY: + /* in-memory attribute file doesn't need data */ + break; + case GIT_ATTR_FILE_SOURCE_INDEX: { + if ((error = attr_file_oid_from_index(&id, repo, entry->path)) < 0 || + (error = git_blob_lookup(&blob, repo, &id)) < 0) + return error; + + /* Do not assume that data straight from the ODB is NULL-terminated; + * copy the contents of a file to a buffer to work on */ + blobsize = git_blob_rawsize(blob); + + GIT_ERROR_CHECK_BLOBSIZE(blobsize); + git_str_put(&content, git_blob_rawcontent(blob), (size_t)blobsize); + break; + } + case GIT_ATTR_FILE_SOURCE_FILE: { + int fd = -1; + + /* For open or read errors, pretend that we got ENOTFOUND. */ + /* TODO: issue warning when warning API is available */ + + if (p_stat(entry->fullpath, &st) < 0 || + S_ISDIR(st.st_mode) || + (fd = git_futils_open_ro(entry->fullpath)) < 0 || + (error = git_futils_readbuffer_fd(&content, fd, (size_t)st.st_size)) < 0) + nonexistent = true; + + if (fd >= 0) + p_close(fd); + + break; + } + case GIT_ATTR_FILE_SOURCE_HEAD: + case GIT_ATTR_FILE_SOURCE_COMMIT: { + if (source->type == GIT_ATTR_FILE_SOURCE_COMMIT) { + if ((error = git_commit_lookup(&commit, repo, source->commit_id)) < 0 || + (error = git_commit_tree(&tree, commit)) < 0) + goto cleanup; + } else { + if ((error = git_repository_head_tree(&tree, repo)) < 0) + goto cleanup; + } + + if ((error = git_tree_entry_bypath(&tree_entry, tree, entry->path)) < 0) { + /* + * If the attributes file does not exist, we can + * cache an empty file for this commit to prevent + * needless future lookups. + */ + if (error == GIT_ENOTFOUND) { + error = 0; + break; + } + + goto cleanup; + } + + if ((error = git_blob_lookup(&blob, repo, git_tree_entry_id(tree_entry))) < 0) + goto cleanup; + + /* + * Do not assume that data straight from the ODB is NULL-terminated; + * copy the contents of a file to a buffer to work on. + */ + blobsize = git_blob_rawsize(blob); + + GIT_ERROR_CHECK_BLOBSIZE(blobsize); + if ((error = git_str_put(&content, + git_blob_rawcontent(blob), (size_t)blobsize)) < 0) + goto cleanup; + + break; + } + default: + git_error_set(GIT_ERROR_INVALID, "unknown file source %d", source->type); + return -1; + } + + if ((error = git_attr_file__new(&file, entry, source)) < 0) + goto cleanup; + + /* advance over a UTF8 BOM */ + content_str = git_str_cstr(&content); + bom_offset = git_str_detect_bom(&bom, &content); + + if (bom == GIT_STR_BOM_UTF8) + content_str += bom_offset; + + /* store the key of the attr_reader; don't bother with cache + * invalidation during the same attr reader session. + */ + if (attr_session) + file->session_key = attr_session->key; + + if (parser && (error = parser(repo, file, content_str, allow_macros)) < 0) { + git_attr_file__free(file); + goto cleanup; + } + + /* write cache breakers */ + if (nonexistent) + file->nonexistent = 1; + else if (source->type == GIT_ATTR_FILE_SOURCE_INDEX) + git_oid_cpy(&file->cache_data.oid, git_blob_id(blob)); + else if (source->type == GIT_ATTR_FILE_SOURCE_HEAD) + git_oid_cpy(&file->cache_data.oid, git_tree_id(tree)); + else if (source->type == GIT_ATTR_FILE_SOURCE_COMMIT) + git_oid_cpy(&file->cache_data.oid, git_tree_id(tree)); + else if (source->type == GIT_ATTR_FILE_SOURCE_FILE) + git_futils_filestamp_set_from_stat(&file->cache_data.stamp, &st); + /* else always cacheable */ + + *out = file; + +cleanup: + git_blob_free(blob); + git_tree_entry_free(tree_entry); + git_tree_free(tree); + git_commit_free(commit); + git_str_dispose(&content); + + return error; +} + +int git_attr_file__out_of_date( + git_repository *repo, + git_attr_session *attr_session, + git_attr_file *file, + git_attr_file_source *source) +{ + if (!file) + return 1; + + /* we are never out of date if we just created this data in the same + * attr_session; otherwise, nonexistent files must be invalidated + */ + if (attr_session && attr_session->key == file->session_key) + return 0; + else if (file->nonexistent) + return 1; + + switch (file->source.type) { + case GIT_ATTR_FILE_SOURCE_MEMORY: + return 0; + + case GIT_ATTR_FILE_SOURCE_FILE: + return git_futils_filestamp_check( + &file->cache_data.stamp, file->entry->fullpath); + + case GIT_ATTR_FILE_SOURCE_INDEX: { + int error; + git_oid id; + + if ((error = attr_file_oid_from_index( + &id, repo, file->entry->path)) < 0) + return error; + + return (git_oid__cmp(&file->cache_data.oid, &id) != 0); + } + + case GIT_ATTR_FILE_SOURCE_HEAD: { + git_tree *tree = NULL; + int error = git_repository_head_tree(&tree, repo); + + if (error < 0) + return error; + + error = (git_oid__cmp(&file->cache_data.oid, git_tree_id(tree)) != 0); + + git_tree_free(tree); + return error; + } + + case GIT_ATTR_FILE_SOURCE_COMMIT: { + git_commit *commit = NULL; + git_tree *tree = NULL; + int error; + + if ((error = git_commit_lookup(&commit, repo, source->commit_id)) < 0) + return error; + + error = git_commit_tree(&tree, commit); + git_commit_free(commit); + + if (error < 0) + return error; + + error = (git_oid__cmp(&file->cache_data.oid, git_tree_id(tree)) != 0); + + git_tree_free(tree); + return error; + } + + default: + git_error_set(GIT_ERROR_INVALID, "invalid file type %d", file->source.type); + return -1; + } +} + +static int sort_by_hash_and_name(const void *a_raw, const void *b_raw); +static void git_attr_rule__clear(git_attr_rule *rule); +static bool parse_optimized_patterns( + git_attr_fnmatch *spec, + git_pool *pool, + const char *pattern); + +int git_attr_file__parse_buffer( + git_repository *repo, git_attr_file *attrs, const char *data, bool allow_macros) +{ + const char *scan = data, *context = NULL; + git_attr_rule *rule = NULL; + int error = 0; + + /* If subdir file path, convert context for file paths */ + if (attrs->entry && git_fs_path_root(attrs->entry->path) < 0 && + !git__suffixcmp(attrs->entry->path, "/" GIT_ATTR_FILE)) + context = attrs->entry->path; + + if (git_mutex_lock(&attrs->lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock attribute file"); + return -1; + } + + while (!error && *scan) { + /* Allocate rule if needed, otherwise re-use previous rule */ + if (!rule) { + rule = git__calloc(1, sizeof(*rule)); + GIT_ERROR_CHECK_ALLOC(rule); + } else + git_attr_rule__clear(rule); + + rule->match.flags = GIT_ATTR_FNMATCH_ALLOWNEG | GIT_ATTR_FNMATCH_ALLOWMACRO; + + /* Parse the next "pattern attr attr attr" line */ + if ((error = git_attr_fnmatch__parse(&rule->match, &attrs->pool, context, &scan)) < 0 || + (error = git_attr_assignment__parse(repo, &attrs->pool, &rule->assigns, &scan)) < 0) + { + if (error != GIT_ENOTFOUND) + goto out; + error = 0; + continue; + } + + if (rule->match.flags & GIT_ATTR_FNMATCH_MACRO) { + /* TODO: warning if macro found in file below repo root */ + if (!allow_macros) + continue; + if ((error = git_attr_cache__insert_macro(repo, rule)) < 0) + goto out; + } else if ((error = git_vector_insert(&attrs->rules, rule)) < 0) + goto out; + + rule = NULL; + } + +out: + git_mutex_unlock(&attrs->lock); + git_attr_rule__free(rule); + + return error; +} + +uint32_t git_attr_file__name_hash(const char *name) +{ + uint32_t h = 5381; + int c; + + GIT_ASSERT_ARG(name); + + while ((c = (int)*name++) != 0) + h = ((h << 5) + h) + c; + return h; +} + +int git_attr_file__lookup_one( + git_attr_file *file, + git_attr_path *path, + const char *attr, + const char **value) +{ + size_t i; + git_attr_name name; + git_attr_rule *rule; + + *value = NULL; + + name.name = attr; + name.name_hash = git_attr_file__name_hash(attr); + + git_attr_file__foreach_matching_rule(file, path, i, rule) { + size_t pos; + + if (!git_vector_bsearch(&pos, &rule->assigns, &name)) { + *value = ((git_attr_assignment *) + git_vector_get(&rule->assigns, pos))->value; + break; + } + } + + return 0; +} + +int git_attr_file__load_standalone(git_attr_file **out, const char *path) +{ + git_str content = GIT_STR_INIT; + git_attr_file_source source = { GIT_ATTR_FILE_SOURCE_FILE }; + git_attr_file *file = NULL; + int error; + + if ((error = git_futils_readbuffer(&content, path)) < 0) + goto out; + + /* + * Because the cache entry is allocated from the file's own pool, we + * don't have to free it - freeing file+pool will free cache entry, too. + */ + + if ((error = git_attr_file__new(&file, NULL, &source)) < 0 || + (error = git_attr_file__parse_buffer(NULL, file, content.ptr, true)) < 0 || + (error = git_attr_cache__alloc_file_entry(&file->entry, NULL, NULL, path, &file->pool)) < 0) + goto out; + + *out = file; +out: + if (error < 0) + git_attr_file__free(file); + git_str_dispose(&content); + + return error; +} + +bool git_attr_fnmatch__match( + git_attr_fnmatch *match, + git_attr_path *path) +{ + const char *relpath = path->path; + const char *filename; + int flags = 0; + + /* + * If the rule was generated in a subdirectory, we must only + * use it for paths inside that directory. We can thus return + * a non-match if the prefixes don't match. + */ + if (match->containing_dir) { + if (match->flags & GIT_ATTR_FNMATCH_ICASE) { + if (git__strncasecmp(path->path, match->containing_dir, match->containing_dir_length)) + return 0; + } else { + if (git__prefixcmp(path->path, match->containing_dir)) + return 0; + } + + relpath += match->containing_dir_length; + } + + if (match->flags & GIT_ATTR_FNMATCH_ICASE) + flags |= WM_CASEFOLD; + + if (match->flags & GIT_ATTR_FNMATCH_FULLPATH) { + filename = relpath; + flags |= WM_PATHNAME; + } else { + filename = path->basename; + } + + if ((match->flags & GIT_ATTR_FNMATCH_DIRECTORY) && !path->is_dir) { + bool samename; + + /* + * for attribute checks or checks at the root of this match's + * containing_dir (or root of the repository if no containing_dir), + * do not match. + */ + if (!(match->flags & GIT_ATTR_FNMATCH_IGNORE) || + path->basename == relpath) + return false; + + /* fail match if this is a file with same name as ignored folder */ + samename = (match->flags & GIT_ATTR_FNMATCH_ICASE) ? + !strcasecmp(match->pattern, relpath) : + !strcmp(match->pattern, relpath); + + if (samename) + return false; + + return (wildmatch(match->pattern, relpath, flags) == WM_MATCH); + } + + return (wildmatch(match->pattern, filename, flags) == WM_MATCH); +} + +bool git_attr_rule__match( + git_attr_rule *rule, + git_attr_path *path) +{ + bool matched = git_attr_fnmatch__match(&rule->match, path); + + if (rule->match.flags & GIT_ATTR_FNMATCH_NEGATIVE) + matched = !matched; + + return matched; +} + +git_attr_assignment *git_attr_rule__lookup_assignment( + git_attr_rule *rule, const char *name) +{ + size_t pos; + git_attr_name key; + key.name = name; + key.name_hash = git_attr_file__name_hash(name); + + if (git_vector_bsearch(&pos, &rule->assigns, &key)) + return NULL; + + return git_vector_get(&rule->assigns, pos); +} + +int git_attr_path__init( + git_attr_path *info, + const char *path, + const char *base, + git_dir_flag dir_flag) +{ + ssize_t root; + + /* build full path as best we can */ + git_str_init(&info->full, 0); + + if (git_fs_path_join_unrooted(&info->full, path, base, &root) < 0) + return -1; + + info->path = info->full.ptr + root; + + /* remove trailing slashes */ + while (info->full.size > 0) { + if (info->full.ptr[info->full.size - 1] != '/') + break; + info->full.size--; + } + info->full.ptr[info->full.size] = '\0'; + + /* skip leading slashes in path */ + while (*info->path == '/') + info->path++; + + /* find trailing basename component */ + info->basename = strrchr(info->path, '/'); + if (info->basename) + info->basename++; + if (!info->basename || !*info->basename) + info->basename = info->path; + + switch (dir_flag) + { + case GIT_DIR_FLAG_FALSE: + info->is_dir = 0; + break; + + case GIT_DIR_FLAG_TRUE: + info->is_dir = 1; + break; + + case GIT_DIR_FLAG_UNKNOWN: + default: + info->is_dir = (int)git_fs_path_isdir(info->full.ptr); + break; + } + + return 0; +} + +void git_attr_path__free(git_attr_path *info) +{ + git_str_dispose(&info->full); + info->path = NULL; + info->basename = NULL; +} + +/* + * From gitattributes(5): + * + * Patterns have the following format: + * + * - A blank line matches no files, so it can serve as a separator for + * readability. + * + * - A line starting with # serves as a comment. + * + * - An optional prefix ! which negates the pattern; any matching file + * excluded by a previous pattern will become included again. If a negated + * pattern matches, this will override lower precedence patterns sources. + * + * - If the pattern ends with a slash, it is removed for the purpose of the + * following description, but it would only find a match with a directory. In + * other words, foo/ will match a directory foo and paths underneath it, but + * will not match a regular file or a symbolic link foo (this is consistent + * with the way how pathspec works in general in git). + * + * - If the pattern does not contain a slash /, git treats it as a shell glob + * pattern and checks for a match against the pathname without leading + * directories. + * + * - Otherwise, git treats the pattern as a shell glob suitable for consumption + * by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will + * not match a / in the pathname. For example, "Documentation/\*.html" matches + * "Documentation/git.html" but not "Documentation/ppc/ppc.html". A leading + * slash matches the beginning of the pathname; for example, "/\*.c" matches + * "cat-file.c" but not "mozilla-sha1/sha1.c". + */ + +/* + * Determine the length of trailing spaces. Escaped spaces do not count as + * trailing whitespace. + */ +static size_t trailing_space_length(const char *p, size_t len) +{ + size_t n, i; + for (n = len; n; n--) { + if (p[n-1] != ' ' && p[n-1] != '\t') + break; + + /* + * Count escape-characters before space. In case where it's an + * even number of escape characters, then the escape char itself + * is escaped and the whitespace is an unescaped whitespace. + * Otherwise, the last escape char is not escaped and the + * whitespace in an escaped whitespace. + */ + i = n; + while (i > 1 && p[i-2] == '\\') + i--; + if ((n - i) % 2) + break; + } + return len - n; +} + +static size_t unescape_spaces(char *str) +{ + char *scan, *pos = str; + bool escaped = false; + + if (!str) + return 0; + + for (scan = str; *scan; scan++) { + if (!escaped && *scan == '\\') { + escaped = true; + continue; + } + + /* Only insert the escape character for escaped non-spaces */ + if (escaped && !git__isspace(*scan)) + *pos++ = '\\'; + + *pos++ = *scan; + escaped = false; + } + + if (pos != scan) + *pos = '\0'; + + return (pos - str); +} + +/* + * This will return 0 if the spec was filled out, + * GIT_ENOTFOUND if the fnmatch does not require matching, or + * another error code there was an actual problem. + */ +int git_attr_fnmatch__parse( + git_attr_fnmatch *spec, + git_pool *pool, + const char *context, + const char **base) +{ + const char *pattern, *scan; + int slash_count, allow_space; + bool escaped; + + GIT_ASSERT_ARG(spec); + GIT_ASSERT_ARG(base && *base); + + if (parse_optimized_patterns(spec, pool, *base)) + return 0; + + spec->flags = (spec->flags & GIT_ATTR_FNMATCH__INCOMING); + allow_space = ((spec->flags & GIT_ATTR_FNMATCH_ALLOWSPACE) != 0); + + pattern = *base; + + while (!allow_space && git__isspace(*pattern)) + pattern++; + + if (!*pattern || *pattern == '#' || *pattern == '\n' || + (*pattern == '\r' && *(pattern + 1) == '\n')) { + *base = git__next_line(pattern); + return GIT_ENOTFOUND; + } + + if (*pattern == '[' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWMACRO) != 0) { + if (strncmp(pattern, "[attr]", 6) == 0) { + spec->flags = spec->flags | GIT_ATTR_FNMATCH_MACRO; + pattern += 6; + } + /* else a character range like [a-e]* which is accepted */ + } + + if (*pattern == '!' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWNEG) != 0) { + spec->flags = spec->flags | GIT_ATTR_FNMATCH_NEGATIVE; + pattern++; + } + + slash_count = 0; + escaped = false; + /* Scan until a non-escaped whitespace. */ + for (scan = pattern; *scan != '\0'; ++scan) { + char c = *scan; + + if (c == '\\' && !escaped) { + escaped = true; + continue; + } else if (git__isspace(c) && !escaped) { + if (!allow_space || (c != ' ' && c != '\t' && c != '\r')) + break; + } else if (c == '/') { + spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH; + slash_count++; + + if (slash_count == 1 && pattern == scan) + pattern++; + } else if (git__iswildcard(c) && !escaped) { + /* remember if we see an unescaped wildcard in pattern */ + spec->flags = spec->flags | GIT_ATTR_FNMATCH_HASWILD; + } + + escaped = false; + } + + *base = scan; + + if ((spec->length = scan - pattern) == 0) + return GIT_ENOTFOUND; + + /* + * Remove one trailing \r in case this is a CRLF delimited + * file, in the case of Icon\r\r\n, we still leave the first + * \r there to match against. + */ + if (pattern[spec->length - 1] == '\r') + if (--spec->length == 0) + return GIT_ENOTFOUND; + + /* Remove trailing spaces. */ + spec->length -= trailing_space_length(pattern, spec->length); + + if (spec->length == 0) + return GIT_ENOTFOUND; + + if (pattern[spec->length - 1] == '/') { + spec->length--; + spec->flags = spec->flags | GIT_ATTR_FNMATCH_DIRECTORY; + if (--slash_count <= 0) + spec->flags = spec->flags & ~GIT_ATTR_FNMATCH_FULLPATH; + } + + if (context) { + char *slash = strrchr(context, '/'); + size_t len; + if (slash) { + /* include the slash for easier matching */ + len = slash - context + 1; + spec->containing_dir = git_pool_strndup(pool, context, len); + spec->containing_dir_length = len; + } + } + + spec->pattern = git_pool_strndup(pool, pattern, spec->length); + + if (!spec->pattern) { + *base = git__next_line(pattern); + return -1; + } else { + /* strip '\' that might have been used for internal whitespace */ + spec->length = unescape_spaces(spec->pattern); + } + + return 0; +} + +static bool parse_optimized_patterns( + git_attr_fnmatch *spec, + git_pool *pool, + const char *pattern) +{ + if (!pattern[1] && (pattern[0] == '*' || pattern[0] == '.')) { + spec->flags = GIT_ATTR_FNMATCH_MATCH_ALL; + spec->pattern = git_pool_strndup(pool, pattern, 1); + spec->length = 1; + + return true; + } + + return false; +} + +static int sort_by_hash_and_name(const void *a_raw, const void *b_raw) +{ + const git_attr_name *a = a_raw; + const git_attr_name *b = b_raw; + + if (b->name_hash < a->name_hash) + return 1; + else if (b->name_hash > a->name_hash) + return -1; + else + return strcmp(b->name, a->name); +} + +static void git_attr_assignment__free(git_attr_assignment *assign) +{ + /* name and value are stored in a git_pool associated with the + * git_attr_file, so they do not need to be freed here + */ + assign->name = NULL; + assign->value = NULL; + git__free(assign); +} + +static int merge_assignments(void **old_raw, void *new_raw) +{ + git_attr_assignment **old = (git_attr_assignment **)old_raw; + git_attr_assignment *new = (git_attr_assignment *)new_raw; + + GIT_REFCOUNT_DEC(*old, git_attr_assignment__free); + *old = new; + return GIT_EEXISTS; +} + +int git_attr_assignment__parse( + git_repository *repo, + git_pool *pool, + git_vector *assigns, + const char **base) +{ + int error; + const char *scan = *base; + git_attr_assignment *assign = NULL; + + GIT_ASSERT_ARG(assigns && !assigns->length); + + git_vector_set_cmp(assigns, sort_by_hash_and_name); + + while (*scan && *scan != '\n') { + const char *name_start, *value_start; + + /* skip leading blanks */ + while (git__isspace(*scan) && *scan != '\n') scan++; + + /* allocate assign if needed */ + if (!assign) { + assign = git__calloc(1, sizeof(git_attr_assignment)); + GIT_ERROR_CHECK_ALLOC(assign); + GIT_REFCOUNT_INC(assign); + } + + assign->name_hash = 5381; + assign->value = git_attr__true; + + /* look for magic name prefixes */ + if (*scan == '-') { + assign->value = git_attr__false; + scan++; + } else if (*scan == '!') { + assign->value = git_attr__unset; /* explicit unspecified state */ + scan++; + } else if (*scan == '#') /* comment rest of line */ + break; + + /* find the name */ + name_start = scan; + while (*scan && !git__isspace(*scan) && *scan != '=') { + assign->name_hash = + ((assign->name_hash << 5) + assign->name_hash) + *scan; + scan++; + } + if (scan == name_start) { + /* must have found lone prefix (" - ") or leading = ("=foo") + * or end of buffer -- advance until whitespace and continue + */ + while (*scan && !git__isspace(*scan)) scan++; + continue; + } + + /* allocate permanent storage for name */ + assign->name = git_pool_strndup(pool, name_start, scan - name_start); + GIT_ERROR_CHECK_ALLOC(assign->name); + + /* if there is an equals sign, find the value */ + if (*scan == '=') { + for (value_start = ++scan; *scan && !git__isspace(*scan); ++scan); + + /* if we found a value, allocate permanent storage for it */ + if (scan > value_start) { + assign->value = git_pool_strndup(pool, value_start, scan - value_start); + GIT_ERROR_CHECK_ALLOC(assign->value); + } + } + + /* expand macros (if given a repo with a macro cache) */ + if (repo != NULL && assign->value == git_attr__true) { + git_attr_rule *macro = + git_attr_cache__lookup_macro(repo, assign->name); + + if (macro != NULL) { + unsigned int i; + git_attr_assignment *massign; + + git_vector_foreach(¯o->assigns, i, massign) { + GIT_REFCOUNT_INC(massign); + + error = git_vector_insert_sorted( + assigns, massign, &merge_assignments); + if (error < 0 && error != GIT_EEXISTS) { + git_attr_assignment__free(assign); + return error; + } + } + } + } + + /* insert allocated assign into vector */ + error = git_vector_insert_sorted(assigns, assign, &merge_assignments); + if (error < 0 && error != GIT_EEXISTS) + return error; + + /* clear assign since it is now "owned" by the vector */ + assign = NULL; + } + + if (assign != NULL) + git_attr_assignment__free(assign); + + *base = git__next_line(scan); + + return (assigns->length == 0) ? GIT_ENOTFOUND : 0; +} + +static void git_attr_rule__clear(git_attr_rule *rule) +{ + unsigned int i; + git_attr_assignment *assign; + + if (!rule) + return; + + if (!(rule->match.flags & GIT_ATTR_FNMATCH_IGNORE)) { + git_vector_foreach(&rule->assigns, i, assign) + GIT_REFCOUNT_DEC(assign, git_attr_assignment__free); + git_vector_free(&rule->assigns); + } + + /* match.pattern is stored in a git_pool, so no need to free */ + rule->match.pattern = NULL; + rule->match.length = 0; +} + +void git_attr_rule__free(git_attr_rule *rule) +{ + git_attr_rule__clear(rule); + git__free(rule); +} + +int git_attr_session__init(git_attr_session *session, git_repository *repo) +{ + GIT_ASSERT_ARG(repo); + + memset(session, 0, sizeof(*session)); + session->key = git_atomic32_inc(&repo->attr_session_key); + + return 0; +} + +void git_attr_session__free(git_attr_session *session) +{ + if (!session) + return; + + git_str_dispose(&session->sysdir); + git_str_dispose(&session->tmp); + + memset(session, 0, sizeof(git_attr_session)); +} diff --git a/src/libgit2/attr_file.h b/src/libgit2/attr_file.h new file mode 100644 index 0000000..08630d1 --- /dev/null +++ b/src/libgit2/attr_file.h @@ -0,0 +1,241 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_attr_file_h__ +#define INCLUDE_attr_file_h__ + +#include "common.h" + +#include "git2/oid.h" +#include "git2/attr.h" +#include "vector.h" +#include "pool.h" +#include "str.h" +#include "futils.h" + +#define GIT_ATTR_FILE ".gitattributes" +#define GIT_ATTR_FILE_INREPO "attributes" +#define GIT_ATTR_FILE_SYSTEM "gitattributes" +#define GIT_ATTR_FILE_XDG "attributes" + +#define GIT_ATTR_FNMATCH_NEGATIVE (1U << 0) +#define GIT_ATTR_FNMATCH_DIRECTORY (1U << 1) +#define GIT_ATTR_FNMATCH_FULLPATH (1U << 2) +#define GIT_ATTR_FNMATCH_MACRO (1U << 3) +#define GIT_ATTR_FNMATCH_IGNORE (1U << 4) +#define GIT_ATTR_FNMATCH_HASWILD (1U << 5) +#define GIT_ATTR_FNMATCH_ALLOWSPACE (1U << 6) +#define GIT_ATTR_FNMATCH_ICASE (1U << 7) +#define GIT_ATTR_FNMATCH_MATCH_ALL (1U << 8) +#define GIT_ATTR_FNMATCH_ALLOWNEG (1U << 9) +#define GIT_ATTR_FNMATCH_ALLOWMACRO (1U << 10) + +#define GIT_ATTR_FNMATCH__INCOMING \ + (GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG | GIT_ATTR_FNMATCH_ALLOWMACRO) + +typedef enum { + GIT_ATTR_FILE_SOURCE_MEMORY = 0, + GIT_ATTR_FILE_SOURCE_FILE = 1, + GIT_ATTR_FILE_SOURCE_INDEX = 2, + GIT_ATTR_FILE_SOURCE_HEAD = 3, + GIT_ATTR_FILE_SOURCE_COMMIT = 4, + + GIT_ATTR_FILE_NUM_SOURCES = 5 +} git_attr_file_source_t; + +typedef struct { + /* The source location for the attribute file. */ + git_attr_file_source_t type; + + /* + * The filename of the attribute file to read (relative to the + * given base path). + */ + const char *base; + const char *filename; + + /* + * The commit ID when the given source type is a commit (or NULL + * for the repository's HEAD commit.) + */ + git_oid *commit_id; +} git_attr_file_source; + +extern const char *git_attr__true; +extern const char *git_attr__false; +extern const char *git_attr__unset; + +typedef struct { + char *pattern; + size_t length; + char *containing_dir; + size_t containing_dir_length; + unsigned int flags; +} git_attr_fnmatch; + +typedef struct { + git_attr_fnmatch match; + git_vector assigns; /* vector of */ +} git_attr_rule; + +typedef struct { + git_refcount unused; + const char *name; + uint32_t name_hash; +} git_attr_name; + +typedef struct { + git_refcount rc; /* for macros */ + char *name; + uint32_t name_hash; + const char *value; +} git_attr_assignment; + +typedef struct git_attr_file_entry git_attr_file_entry; + +typedef struct { + git_refcount rc; + git_mutex lock; + git_attr_file_entry *entry; + git_attr_file_source source; + git_vector rules; /* vector of or */ + git_pool pool; + unsigned int nonexistent:1; + int session_key; + union { + git_oid oid; + git_futils_filestamp stamp; + } cache_data; +} git_attr_file; + +struct git_attr_file_entry { + git_attr_file *file[GIT_ATTR_FILE_NUM_SOURCES]; + const char *path; /* points into fullpath */ + char fullpath[GIT_FLEX_ARRAY]; +}; + +typedef struct { + git_str full; + char *path; + char *basename; + int is_dir; +} git_attr_path; + +/* A git_attr_session can provide an "instance" of reading, to prevent cache + * invalidation during a single operation instance (like checkout). + */ + +typedef struct { + int key; + unsigned int init_setup:1, + init_sysdir:1; + git_str sysdir; + git_str tmp; +} git_attr_session; + +extern int git_attr_session__init(git_attr_session *attr_session, git_repository *repo); +extern void git_attr_session__free(git_attr_session *session); + +extern int git_attr_get_many_with_session( + const char **values_out, + git_repository *repo, + git_attr_session *attr_session, + git_attr_options *opts, + const char *path, + size_t num_attr, + const char **names); + +typedef int (*git_attr_file_parser)( + git_repository *repo, + git_attr_file *file, + const char *data, + bool allow_macros); + +/* + * git_attr_file API + */ + +int git_attr_file__new( + git_attr_file **out, + git_attr_file_entry *entry, + git_attr_file_source *source); + +void git_attr_file__free(git_attr_file *file); + +int git_attr_file__load( + git_attr_file **out, + git_repository *repo, + git_attr_session *attr_session, + git_attr_file_entry *ce, + git_attr_file_source *source, + git_attr_file_parser parser, + bool allow_macros); + +int git_attr_file__load_standalone( + git_attr_file **out, const char *path); + +int git_attr_file__out_of_date( + git_repository *repo, git_attr_session *session, git_attr_file *file, git_attr_file_source *source); + +int git_attr_file__parse_buffer( + git_repository *repo, git_attr_file *attrs, const char *data, bool allow_macros); + +int git_attr_file__clear_rules( + git_attr_file *file, bool need_lock); + +int git_attr_file__lookup_one( + git_attr_file *file, + git_attr_path *path, + const char *attr, + const char **value); + +/* loop over rules in file from bottom to top */ +#define git_attr_file__foreach_matching_rule(file, path, iter, rule) \ + git_vector_rforeach(&(file)->rules, (iter), (rule)) \ + if (git_attr_rule__match((rule), (path))) + +uint32_t git_attr_file__name_hash(const char *name); + + +/* + * other utilities + */ + +extern int git_attr_fnmatch__parse( + git_attr_fnmatch *spec, + git_pool *pool, + const char *source, + const char **base); + +extern bool git_attr_fnmatch__match( + git_attr_fnmatch *rule, + git_attr_path *path); + +extern void git_attr_rule__free(git_attr_rule *rule); + +extern bool git_attr_rule__match( + git_attr_rule *rule, + git_attr_path *path); + +extern git_attr_assignment *git_attr_rule__lookup_assignment( + git_attr_rule *rule, const char *name); + +typedef enum { GIT_DIR_FLAG_TRUE = 1, GIT_DIR_FLAG_FALSE = 0, GIT_DIR_FLAG_UNKNOWN = -1 } git_dir_flag; + +extern int git_attr_path__init( + git_attr_path *out, + const char *path, + const char *base, + git_dir_flag is_dir); +extern void git_attr_path__free(git_attr_path *info); + +extern int git_attr_assignment__parse( + git_repository *repo, /* needed to expand macros */ + git_pool *pool, + git_vector *assigns, + const char **scan); + +#endif diff --git a/src/libgit2/attrcache.c b/src/libgit2/attrcache.c new file mode 100644 index 0000000..405944e --- /dev/null +++ b/src/libgit2/attrcache.c @@ -0,0 +1,478 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "attrcache.h" + +#include "repository.h" +#include "attr_file.h" +#include "config.h" +#include "sysdir.h" +#include "ignore.h" +#include "path.h" + +GIT_INLINE(int) attr_cache_lock(git_attr_cache *cache) +{ + GIT_UNUSED(cache); /* avoid warning if threading is off */ + + if (git_mutex_lock(&cache->lock) < 0) { + git_error_set(GIT_ERROR_OS, "unable to get attr cache lock"); + return -1; + } + return 0; +} + +GIT_INLINE(void) attr_cache_unlock(git_attr_cache *cache) +{ + GIT_UNUSED(cache); /* avoid warning if threading is off */ + git_mutex_unlock(&cache->lock); +} + +GIT_INLINE(git_attr_file_entry *) attr_cache_lookup_entry( + git_attr_cache *cache, const char *path) +{ + return git_strmap_get(cache->files, path); +} + +int git_attr_cache__alloc_file_entry( + git_attr_file_entry **out, + git_repository *repo, + const char *base, + const char *path, + git_pool *pool) +{ + git_str fullpath_str = GIT_STR_INIT; + size_t baselen = 0, pathlen = strlen(path); + size_t cachesize = sizeof(git_attr_file_entry) + pathlen + 1; + git_attr_file_entry *ce; + + if (base != NULL && git_fs_path_root(path) < 0) { + baselen = strlen(base); + cachesize += baselen; + + if (baselen && base[baselen - 1] != '/') + cachesize++; + } + + ce = git_pool_mallocz(pool, cachesize); + GIT_ERROR_CHECK_ALLOC(ce); + + if (baselen) { + memcpy(ce->fullpath, base, baselen); + + if (base[baselen - 1] != '/') + ce->fullpath[baselen++] = '/'; + } + memcpy(&ce->fullpath[baselen], path, pathlen); + + fullpath_str.ptr = ce->fullpath; + fullpath_str.size = pathlen + baselen; + + if (git_path_validate_str_length(repo, &fullpath_str) < 0) + return -1; + + ce->path = &ce->fullpath[baselen]; + *out = ce; + + return 0; +} + +/* call with attrcache locked */ +static int attr_cache_make_entry( + git_attr_file_entry **out, git_repository *repo, const char *path) +{ + git_attr_cache *cache = git_repository_attr_cache(repo); + git_attr_file_entry *entry = NULL; + int error; + + if ((error = git_attr_cache__alloc_file_entry(&entry, repo, + git_repository_workdir(repo), path, &cache->pool)) < 0) + return error; + + if ((error = git_strmap_set(cache->files, entry->path, entry)) < 0) + return error; + + *out = entry; + return error; +} + +/* insert entry or replace existing if we raced with another thread */ +static int attr_cache_upsert(git_attr_cache *cache, git_attr_file *file) +{ + git_attr_file_entry *entry; + git_attr_file *old; + + if (attr_cache_lock(cache) < 0) + return -1; + + entry = attr_cache_lookup_entry(cache, file->entry->path); + + GIT_REFCOUNT_OWN(file, entry); + GIT_REFCOUNT_INC(file); + + /* + * Replace the existing value if another thread has + * created it in the meantime. + */ + old = git_atomic_swap(entry->file[file->source.type], file); + + if (old) { + GIT_REFCOUNT_OWN(old, NULL); + git_attr_file__free(old); + } + + attr_cache_unlock(cache); + return 0; +} + +static int attr_cache_remove(git_attr_cache *cache, git_attr_file *file) +{ + int error = 0; + git_attr_file_entry *entry; + git_attr_file *oldfile = NULL; + + if (!file) + return 0; + + if ((error = attr_cache_lock(cache)) < 0) + return error; + + if ((entry = attr_cache_lookup_entry(cache, file->entry->path)) != NULL) + oldfile = git_atomic_compare_and_swap(&entry->file[file->source.type], file, NULL); + + attr_cache_unlock(cache); + + if (oldfile == file) { + GIT_REFCOUNT_OWN(file, NULL); + git_attr_file__free(file); + } + + return error; +} + +/* Look up cache entry and file. + * - If entry is not present, create it while the cache is locked. + * - If file is present, increment refcount before returning it, so the + * cache can be unlocked and it won't go away. + */ +static int attr_cache_lookup( + git_attr_file **out_file, + git_attr_file_entry **out_entry, + git_repository *repo, + git_attr_session *attr_session, + git_attr_file_source *source) +{ + int error = 0; + git_str path = GIT_STR_INIT; + const char *wd = git_repository_workdir(repo); + const char *filename; + git_attr_cache *cache = git_repository_attr_cache(repo); + git_attr_file_entry *entry = NULL; + git_attr_file *file = NULL; + + /* join base and path as needed */ + if (source->base != NULL && git_fs_path_root(source->filename) < 0) { + git_str *p = attr_session ? &attr_session->tmp : &path; + + if (git_str_joinpath(p, source->base, source->filename) < 0 || + git_path_validate_str_length(repo, p) < 0) + return -1; + + filename = p->ptr; + } else { + filename = source->filename; + } + + if (wd && !git__prefixcmp(filename, wd)) + filename += strlen(wd); + + /* check cache for existing entry */ + if ((error = attr_cache_lock(cache)) < 0) + goto cleanup; + + entry = attr_cache_lookup_entry(cache, filename); + + if (!entry) { + error = attr_cache_make_entry(&entry, repo, filename); + } else if (entry->file[source->type] != NULL) { + file = entry->file[source->type]; + GIT_REFCOUNT_INC(file); + } + + attr_cache_unlock(cache); + +cleanup: + *out_file = file; + *out_entry = entry; + + git_str_dispose(&path); + return error; +} + +int git_attr_cache__get( + git_attr_file **out, + git_repository *repo, + git_attr_session *attr_session, + git_attr_file_source *source, + git_attr_file_parser parser, + bool allow_macros) +{ + int error = 0; + git_attr_cache *cache = git_repository_attr_cache(repo); + git_attr_file_entry *entry = NULL; + git_attr_file *file = NULL, *updated = NULL; + + if ((error = attr_cache_lookup(&file, &entry, repo, attr_session, source)) < 0) + return error; + + /* load file if we don't have one or if existing one is out of date */ + if (!file || + (error = git_attr_file__out_of_date(repo, attr_session, file, source)) > 0) + error = git_attr_file__load(&updated, repo, attr_session, + entry, source, parser, + allow_macros); + + /* if we loaded the file, insert into and/or update cache */ + if (updated) { + if ((error = attr_cache_upsert(cache, updated)) < 0) { + git_attr_file__free(updated); + } else { + git_attr_file__free(file); /* offset incref from lookup */ + file = updated; + } + } + + /* if file could not be loaded */ + if (error < 0) { + /* remove existing entry */ + if (file) { + attr_cache_remove(cache, file); + git_attr_file__free(file); /* offset incref from lookup */ + file = NULL; + } + /* no error if file simply doesn't exist */ + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = 0; + } + } + + *out = file; + return error; +} + +bool git_attr_cache__is_cached( + git_repository *repo, + git_attr_file_source_t source_type, + const char *filename) +{ + git_attr_cache *cache = git_repository_attr_cache(repo); + git_attr_file_entry *entry; + git_strmap *files; + + if (!cache || !(files = cache->files)) + return false; + + if ((entry = git_strmap_get(files, filename)) == NULL) + return false; + + return entry && (entry->file[source_type] != NULL); +} + + +static int attr_cache__lookup_path( + char **out, git_config *cfg, const char *key, const char *fallback) +{ + git_str buf = GIT_STR_INIT; + int error; + git_config_entry *entry = NULL; + + *out = NULL; + + if ((error = git_config__lookup_entry(&entry, cfg, key, false)) < 0) + return error; + + if (entry) { + const char *cfgval = entry->value; + + /* expand leading ~/ as needed */ + if (cfgval && cfgval[0] == '~' && cfgval[1] == '/') { + if (! (error = git_sysdir_expand_homedir_file(&buf, &cfgval[2]))) + *out = git_str_detach(&buf); + } else if (cfgval) { + *out = git__strdup(cfgval); + } + } + else if (!git_sysdir_find_xdg_file(&buf, fallback)) { + *out = git_str_detach(&buf); + } + + git_config_entry_free(entry); + git_str_dispose(&buf); + + return error; +} + +static void attr_cache__free(git_attr_cache *cache) +{ + bool unlock; + + if (!cache) + return; + + unlock = (attr_cache_lock(cache) == 0); + + if (cache->files != NULL) { + git_attr_file_entry *entry; + git_attr_file *file; + int i; + + git_strmap_foreach_value(cache->files, entry, { + for (i = 0; i < GIT_ATTR_FILE_NUM_SOURCES; ++i) { + if ((file = git_atomic_swap(entry->file[i], NULL)) != NULL) { + GIT_REFCOUNT_OWN(file, NULL); + git_attr_file__free(file); + } + } + }); + git_strmap_free(cache->files); + } + + if (cache->macros != NULL) { + git_attr_rule *rule; + + git_strmap_foreach_value(cache->macros, rule, { + git_attr_rule__free(rule); + }); + git_strmap_free(cache->macros); + } + + git_pool_clear(&cache->pool); + + git__free(cache->cfg_attr_file); + cache->cfg_attr_file = NULL; + + git__free(cache->cfg_excl_file); + cache->cfg_excl_file = NULL; + + if (unlock) + attr_cache_unlock(cache); + git_mutex_free(&cache->lock); + + git__free(cache); +} + +int git_attr_cache__init(git_repository *repo) +{ + int ret = 0; + git_attr_cache *cache = git_repository_attr_cache(repo); + git_config *cfg = NULL; + + if (cache) + return 0; + + cache = git__calloc(1, sizeof(git_attr_cache)); + GIT_ERROR_CHECK_ALLOC(cache); + + /* set up lock */ + if (git_mutex_init(&cache->lock) < 0) { + git_error_set(GIT_ERROR_OS, "unable to initialize lock for attr cache"); + git__free(cache); + return -1; + } + + if ((ret = git_repository_config_snapshot(&cfg, repo)) < 0) + goto cancel; + + /* cache config settings for attributes and ignores */ + ret = attr_cache__lookup_path( + &cache->cfg_attr_file, cfg, GIT_ATTR_CONFIG, GIT_ATTR_FILE_XDG); + if (ret < 0) + goto cancel; + + ret = attr_cache__lookup_path( + &cache->cfg_excl_file, cfg, GIT_IGNORE_CONFIG, GIT_IGNORE_FILE_XDG); + if (ret < 0) + goto cancel; + + /* allocate hashtable for attribute and ignore file contents, + * hashtable for attribute macros, and string pool + */ + if ((ret = git_strmap_new(&cache->files)) < 0 || + (ret = git_strmap_new(&cache->macros)) < 0 || + (ret = git_pool_init(&cache->pool, 1)) < 0) + goto cancel; + + if (git_atomic_compare_and_swap(&repo->attrcache, NULL, cache) != NULL) + goto cancel; /* raced with another thread, free this but no error */ + + git_config_free(cfg); + + /* insert default macros */ + return git_attr_add_macro(repo, "binary", "-diff -merge -text -crlf"); + +cancel: + attr_cache__free(cache); + git_config_free(cfg); + return ret; +} + +int git_attr_cache_flush(git_repository *repo) +{ + git_attr_cache *cache; + + /* this could be done less expensively, but for now, we'll just free + * the entire attrcache and let the next use reinitialize it... + */ + if (repo && (cache = git_atomic_swap(repo->attrcache, NULL)) != NULL) + attr_cache__free(cache); + + return 0; +} + +int git_attr_cache__insert_macro(git_repository *repo, git_attr_rule *macro) +{ + git_attr_cache *cache = git_repository_attr_cache(repo); + git_attr_rule *preexisting; + bool locked = false; + int error = 0; + + /* + * Callers assume that if we return success, that the + * macro will have been adopted by the attributes cache. + * Thus, we have to free the macro here if it's not being + * added to the cache. + * + * TODO: generate warning log if (macro->assigns.length == 0) + */ + if (macro->assigns.length == 0) { + git_attr_rule__free(macro); + goto out; + } + + if ((error = attr_cache_lock(cache)) < 0) + goto out; + locked = true; + + if ((preexisting = git_strmap_get(cache->macros, macro->match.pattern)) != NULL) + git_attr_rule__free(preexisting); + + if ((error = git_strmap_set(cache->macros, macro->match.pattern, macro)) < 0) + goto out; + +out: + if (locked) + attr_cache_unlock(cache); + return error; +} + +git_attr_rule *git_attr_cache__lookup_macro( + git_repository *repo, const char *name) +{ + git_strmap *macros = git_repository_attr_cache(repo)->macros; + + return git_strmap_get(macros, name); +} diff --git a/src/libgit2/attrcache.h b/src/libgit2/attrcache.h new file mode 100644 index 0000000..b13e0e8 --- /dev/null +++ b/src/libgit2/attrcache.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_attrcache_h__ +#define INCLUDE_attrcache_h__ + +#include "common.h" + +#include "attr_file.h" +#include "strmap.h" + +#define GIT_ATTR_CONFIG "core.attributesfile" +#define GIT_IGNORE_CONFIG "core.excludesfile" + +typedef struct { + char *cfg_attr_file; /* cached value of core.attributesfile */ + char *cfg_excl_file; /* cached value of core.excludesfile */ + git_strmap *files; /* hash path to git_attr_cache_entry records */ + git_strmap *macros; /* hash name to vector */ + git_mutex lock; + git_pool pool; +} git_attr_cache; + +extern int git_attr_cache__init(git_repository *repo); + +/* get file - loading and reload as needed */ +extern int git_attr_cache__get( + git_attr_file **file, + git_repository *repo, + git_attr_session *attr_session, + git_attr_file_source *source, + git_attr_file_parser parser, + bool allow_macros); + +extern bool git_attr_cache__is_cached( + git_repository *repo, + git_attr_file_source_t source_type, + const char *filename); + +extern int git_attr_cache__alloc_file_entry( + git_attr_file_entry **out, + git_repository *repo, + const char *base, + const char *path, + git_pool *pool); + +extern int git_attr_cache__insert_macro( + git_repository *repo, git_attr_rule *macro); + +extern git_attr_rule *git_attr_cache__lookup_macro( + git_repository *repo, const char *name); + +#endif diff --git a/src/libgit2/blame.c b/src/libgit2/blame.c new file mode 100644 index 0000000..d93dd5e --- /dev/null +++ b/src/libgit2/blame.c @@ -0,0 +1,566 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "blame.h" + +#include "git2/commit.h" +#include "git2/revparse.h" +#include "git2/revwalk.h" +#include "git2/tree.h" +#include "git2/diff.h" +#include "git2/blob.h" +#include "git2/signature.h" +#include "git2/mailmap.h" +#include "util.h" +#include "repository.h" +#include "blame_git.h" + + +static int hunk_byfinalline_search_cmp(const void *key, const void *entry) +{ + git_blame_hunk *hunk = (git_blame_hunk*)entry; + + size_t lineno = *(size_t*)key; + size_t lines_in_hunk = hunk->lines_in_hunk; + size_t final_start_line_number = hunk->final_start_line_number; + + if (lineno < final_start_line_number) + return -1; + if (lineno >= final_start_line_number + lines_in_hunk) + return 1; + return 0; +} + +static int paths_cmp(const void *a, const void *b) { return git__strcmp((char*)a, (char*)b); } +static int hunk_cmp(const void *_a, const void *_b) +{ + git_blame_hunk *a = (git_blame_hunk*)_a, + *b = (git_blame_hunk*)_b; + + if (a->final_start_line_number > b->final_start_line_number) + return 1; + else if (a->final_start_line_number < b->final_start_line_number) + return -1; + else + return 0; +} + +static bool hunk_ends_at_or_before_line(git_blame_hunk *hunk, size_t line) +{ + return line >= (hunk->final_start_line_number + hunk->lines_in_hunk - 1); +} + +static bool hunk_starts_at_or_after_line(git_blame_hunk *hunk, size_t line) +{ + return line <= hunk->final_start_line_number; +} + +static git_blame_hunk *new_hunk( + size_t start, + size_t lines, + size_t orig_start, + const char *path, + git_blame *blame) +{ + git_blame_hunk *hunk = git__calloc(1, sizeof(git_blame_hunk)); + if (!hunk) return NULL; + + hunk->lines_in_hunk = lines; + hunk->final_start_line_number = start; + hunk->orig_start_line_number = orig_start; + hunk->orig_path = path ? git__strdup(path) : NULL; + git_oid_clear(&hunk->orig_commit_id, blame->repository->oid_type); + git_oid_clear(&hunk->final_commit_id, blame->repository->oid_type); + + return hunk; +} + +static void free_hunk(git_blame_hunk *hunk) +{ + git__free((void*)hunk->orig_path); + git_signature_free(hunk->final_signature); + git_signature_free(hunk->orig_signature); + git__free(hunk); +} + +static git_blame_hunk *dup_hunk(git_blame_hunk *hunk, git_blame *blame) +{ + git_blame_hunk *newhunk = new_hunk( + hunk->final_start_line_number, + hunk->lines_in_hunk, + hunk->orig_start_line_number, + hunk->orig_path, + blame); + + if (!newhunk) + return NULL; + + git_oid_cpy(&newhunk->orig_commit_id, &hunk->orig_commit_id); + git_oid_cpy(&newhunk->final_commit_id, &hunk->final_commit_id); + newhunk->boundary = hunk->boundary; + + if (git_signature_dup(&newhunk->final_signature, hunk->final_signature) < 0 || + git_signature_dup(&newhunk->orig_signature, hunk->orig_signature) < 0) { + free_hunk(newhunk); + return NULL; + } + + return newhunk; +} + +/* Starting with the hunk that includes start_line, shift all following hunks' + * final_start_line by shift_by lines */ +static void shift_hunks_by(git_vector *v, size_t start_line, int shift_by) +{ + size_t i; + + if (!git_vector_bsearch2(&i, v, hunk_byfinalline_search_cmp, &start_line)) { + for (; i < v->length; i++) { + git_blame_hunk *hunk = (git_blame_hunk*)v->contents[i]; + hunk->final_start_line_number += shift_by; + } + } +} + +git_blame *git_blame__alloc( + git_repository *repo, + git_blame_options opts, + const char *path) +{ + git_blame *gbr = git__calloc(1, sizeof(git_blame)); + if (!gbr) + return NULL; + + gbr->repository = repo; + gbr->options = opts; + + if (git_vector_init(&gbr->hunks, 8, hunk_cmp) < 0 || + git_vector_init(&gbr->paths, 8, paths_cmp) < 0 || + (gbr->path = git__strdup(path)) == NULL || + git_vector_insert(&gbr->paths, git__strdup(path)) < 0) + { + git_blame_free(gbr); + return NULL; + } + + if (opts.flags & GIT_BLAME_USE_MAILMAP && + git_mailmap_from_repository(&gbr->mailmap, repo) < 0) { + git_blame_free(gbr); + return NULL; + } + + return gbr; +} + +void git_blame_free(git_blame *blame) +{ + size_t i; + git_blame_hunk *hunk; + + if (!blame) return; + + git_vector_foreach(&blame->hunks, i, hunk) + free_hunk(hunk); + git_vector_free(&blame->hunks); + + git_vector_free_deep(&blame->paths); + + git_array_clear(blame->line_index); + + git_mailmap_free(blame->mailmap); + + git__free(blame->path); + git_blob_free(blame->final_blob); + git__free(blame); +} + +uint32_t git_blame_get_hunk_count(git_blame *blame) +{ + GIT_ASSERT_ARG(blame); + return (uint32_t)blame->hunks.length; +} + +const git_blame_hunk *git_blame_get_hunk_byindex(git_blame *blame, uint32_t index) +{ + GIT_ASSERT_ARG_WITH_RETVAL(blame, NULL); + return (git_blame_hunk*)git_vector_get(&blame->hunks, index); +} + +const git_blame_hunk *git_blame_get_hunk_byline(git_blame *blame, size_t lineno) +{ + size_t i, new_lineno = lineno; + + GIT_ASSERT_ARG_WITH_RETVAL(blame, NULL); + + if (!git_vector_bsearch2(&i, &blame->hunks, hunk_byfinalline_search_cmp, &new_lineno)) { + return git_blame_get_hunk_byindex(blame, (uint32_t)i); + } + + return NULL; +} + +static int normalize_options( + git_blame_options *out, + const git_blame_options *in, + git_repository *repo) +{ + git_blame_options dummy = GIT_BLAME_OPTIONS_INIT; + if (!in) in = &dummy; + + memcpy(out, in, sizeof(git_blame_options)); + + /* No newest_commit => HEAD */ + if (git_oid_is_zero(&out->newest_commit)) { + if (git_reference_name_to_id(&out->newest_commit, repo, "HEAD") < 0) { + return -1; + } + } + + /* min_line 0 really means 1 */ + if (!out->min_line) out->min_line = 1; + /* max_line 0 really means N, but we don't know N yet */ + + /* Fix up option implications */ + if (out->flags & GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES) + out->flags |= GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES; + if (out->flags & GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES) + out->flags |= GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES; + if (out->flags & GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES) + out->flags |= GIT_BLAME_TRACK_COPIES_SAME_FILE; + + return 0; +} + +static git_blame_hunk *split_hunk_in_vector( + git_vector *vec, + git_blame_hunk *hunk, + size_t rel_line, + bool return_new, + git_blame *blame) +{ + size_t new_line_count; + git_blame_hunk *nh; + + /* Don't split if already at a boundary */ + if (rel_line <= 0 || + rel_line >= hunk->lines_in_hunk) + { + return hunk; + } + + new_line_count = hunk->lines_in_hunk - rel_line; + nh = new_hunk(hunk->final_start_line_number + rel_line, + new_line_count, hunk->orig_start_line_number + rel_line, + hunk->orig_path, blame); + + if (!nh) + return NULL; + + git_oid_cpy(&nh->final_commit_id, &hunk->final_commit_id); + git_oid_cpy(&nh->orig_commit_id, &hunk->orig_commit_id); + + /* Adjust hunk that was split */ + hunk->lines_in_hunk -= new_line_count; + git_vector_insert_sorted(vec, nh, NULL); + { + git_blame_hunk *ret = return_new ? nh : hunk; + return ret; + } +} + +/* + * Construct a list of char indices for where lines begin + * Adapted from core git: + * https://github.com/gitster/git/blob/be5c9fb9049ed470e7005f159bb923a5f4de1309/builtin/blame.c#L1760-L1789 + */ +static int index_blob_lines(git_blame *blame) +{ + const char *buf = blame->final_buf; + size_t len = blame->final_buf_size; + int num = 0, incomplete = 0, bol = 1; + size_t *i; + + if (len && buf[len-1] != '\n') + incomplete++; /* incomplete line at the end */ + while (len--) { + if (bol) { + i = git_array_alloc(blame->line_index); + GIT_ERROR_CHECK_ALLOC(i); + *i = buf - blame->final_buf; + bol = 0; + } + if (*buf++ == '\n') { + num++; + bol = 1; + } + } + i = git_array_alloc(blame->line_index); + GIT_ERROR_CHECK_ALLOC(i); + *i = buf - blame->final_buf; + blame->num_lines = num + incomplete; + return blame->num_lines; +} + +static git_blame_hunk *hunk_from_entry(git_blame__entry *e, git_blame *blame) +{ + git_blame_hunk *h = new_hunk( + e->lno+1, e->num_lines, e->s_lno+1, e->suspect->path, + blame); + + if (!h) + return NULL; + + git_oid_cpy(&h->final_commit_id, git_commit_id(e->suspect->commit)); + git_oid_cpy(&h->orig_commit_id, git_commit_id(e->suspect->commit)); + git_commit_author_with_mailmap( + &h->final_signature, e->suspect->commit, blame->mailmap); + git_signature_dup(&h->orig_signature, h->final_signature); + h->boundary = e->is_boundary ? 1 : 0; + return h; +} + +static int load_blob(git_blame *blame) +{ + int error; + + if (blame->final_blob) return 0; + + error = git_commit_lookup(&blame->final, blame->repository, &blame->options.newest_commit); + if (error < 0) + goto cleanup; + error = git_object_lookup_bypath((git_object**)&blame->final_blob, + (git_object*)blame->final, blame->path, GIT_OBJECT_BLOB); + +cleanup: + return error; +} + +static int blame_internal(git_blame *blame) +{ + int error; + git_blame__entry *ent = NULL; + git_blame__origin *o; + + if ((error = load_blob(blame)) < 0 || + (error = git_blame__get_origin(&o, blame, blame->final, blame->path)) < 0) + goto cleanup; + + if (git_blob_rawsize(blame->final_blob) > SIZE_MAX) { + git_error_set(GIT_ERROR_NOMEMORY, "blob is too large to blame"); + error = -1; + goto cleanup; + } + + blame->final_buf = git_blob_rawcontent(blame->final_blob); + blame->final_buf_size = (size_t)git_blob_rawsize(blame->final_blob); + + ent = git__calloc(1, sizeof(git_blame__entry)); + GIT_ERROR_CHECK_ALLOC(ent); + + ent->num_lines = index_blob_lines(blame); + ent->lno = blame->options.min_line - 1; + ent->num_lines = ent->num_lines - blame->options.min_line + 1; + if (blame->options.max_line > 0) + ent->num_lines = blame->options.max_line - blame->options.min_line + 1; + ent->s_lno = ent->lno; + ent->suspect = o; + + blame->ent = ent; + + error = git_blame__like_git(blame, blame->options.flags); + +cleanup: + for (ent = blame->ent; ent; ) { + git_blame__entry *e = ent->next; + git_blame_hunk *h = hunk_from_entry(ent, blame); + + git_vector_insert(&blame->hunks, h); + + git_blame__free_entry(ent); + ent = e; + } + + return error; +} + +/******************************************************************************* + * File blaming + ******************************************************************************/ + +int git_blame_file( + git_blame **out, + git_repository *repo, + const char *path, + git_blame_options *options) +{ + int error = -1; + git_blame_options normOptions = GIT_BLAME_OPTIONS_INIT; + git_blame *blame = NULL; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(path); + + if ((error = normalize_options(&normOptions, options, repo)) < 0) + goto on_error; + + blame = git_blame__alloc(repo, normOptions, path); + GIT_ERROR_CHECK_ALLOC(blame); + + if ((error = load_blob(blame)) < 0) + goto on_error; + + if ((error = blame_internal(blame)) < 0) + goto on_error; + + *out = blame; + return 0; + +on_error: + git_blame_free(blame); + return error; +} + +/******************************************************************************* + * Buffer blaming + *******************************************************************************/ + +static bool hunk_is_bufferblame(git_blame_hunk *hunk) +{ + return hunk && git_oid_is_zero(&hunk->final_commit_id); +} + +static int buffer_hunk_cb( + const git_diff_delta *delta, + const git_diff_hunk *hunk, + void *payload) +{ + git_blame *blame = (git_blame*)payload; + uint32_t wedge_line; + + GIT_UNUSED(delta); + + wedge_line = (hunk->old_lines == 0) ? hunk->new_start : hunk->old_start; + blame->current_diff_line = wedge_line; + + blame->current_hunk = (git_blame_hunk*)git_blame_get_hunk_byline(blame, wedge_line); + if (!blame->current_hunk) { + /* Line added at the end of the file */ + blame->current_hunk = new_hunk(wedge_line, 0, wedge_line, + blame->path, blame); + GIT_ERROR_CHECK_ALLOC(blame->current_hunk); + + git_vector_insert(&blame->hunks, blame->current_hunk); + } else if (!hunk_starts_at_or_after_line(blame->current_hunk, wedge_line)){ + /* If this hunk doesn't start between existing hunks, split a hunk up so it does */ + blame->current_hunk = split_hunk_in_vector(&blame->hunks, blame->current_hunk, + wedge_line - blame->current_hunk->orig_start_line_number, true, + blame); + GIT_ERROR_CHECK_ALLOC(blame->current_hunk); + } + + return 0; +} + +static int ptrs_equal_cmp(const void *a, const void *b) { return ab ? 1 : 0; } +static int buffer_line_cb( + const git_diff_delta *delta, + const git_diff_hunk *hunk, + const git_diff_line *line, + void *payload) +{ + git_blame *blame = (git_blame*)payload; + + GIT_UNUSED(delta); + GIT_UNUSED(hunk); + GIT_UNUSED(line); + + if (line->origin == GIT_DIFF_LINE_ADDITION) { + if (hunk_is_bufferblame(blame->current_hunk) && + hunk_ends_at_or_before_line(blame->current_hunk, blame->current_diff_line)) { + /* Append to the current buffer-blame hunk */ + blame->current_hunk->lines_in_hunk++; + shift_hunks_by(&blame->hunks, blame->current_diff_line+1, 1); + } else { + /* Create a new buffer-blame hunk with this line */ + shift_hunks_by(&blame->hunks, blame->current_diff_line, 1); + blame->current_hunk = new_hunk(blame->current_diff_line, 1, 0, blame->path, blame); + GIT_ERROR_CHECK_ALLOC(blame->current_hunk); + + git_vector_insert_sorted(&blame->hunks, blame->current_hunk, NULL); + } + blame->current_diff_line++; + } + + if (line->origin == GIT_DIFF_LINE_DELETION) { + /* Trim the line from the current hunk; remove it if it's now empty */ + size_t shift_base = blame->current_diff_line + blame->current_hunk->lines_in_hunk+1; + + if (--(blame->current_hunk->lines_in_hunk) == 0) { + size_t i; + shift_base--; + if (!git_vector_search2(&i, &blame->hunks, ptrs_equal_cmp, blame->current_hunk)) { + git_vector_remove(&blame->hunks, i); + free_hunk(blame->current_hunk); + blame->current_hunk = (git_blame_hunk*)git_blame_get_hunk_byindex(blame, (uint32_t)i); + } + } + shift_hunks_by(&blame->hunks, shift_base, -1); + } + return 0; +} + +int git_blame_buffer( + git_blame **out, + git_blame *reference, + const char *buffer, + size_t buffer_len) +{ + git_blame *blame; + git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT; + size_t i; + git_blame_hunk *hunk; + + diffopts.context_lines = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(reference); + GIT_ASSERT_ARG(buffer && buffer_len); + + blame = git_blame__alloc(reference->repository, reference->options, reference->path); + GIT_ERROR_CHECK_ALLOC(blame); + + /* Duplicate all of the hunk structures in the reference blame */ + git_vector_foreach(&reference->hunks, i, hunk) { + git_blame_hunk *h = dup_hunk(hunk, blame); + GIT_ERROR_CHECK_ALLOC(h); + + git_vector_insert(&blame->hunks, h); + } + + /* Diff to the reference blob */ + git_diff_blob_to_buffer(reference->final_blob, blame->path, + buffer, buffer_len, blame->path, &diffopts, + NULL, NULL, buffer_hunk_cb, buffer_line_cb, blame); + + *out = blame; + return 0; +} + +int git_blame_options_init(git_blame_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_blame_options, GIT_BLAME_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_blame_init_options(git_blame_options *opts, unsigned int version) +{ + return git_blame_options_init(opts, version); +} +#endif diff --git a/src/libgit2/blame.h b/src/libgit2/blame.h new file mode 100644 index 0000000..4141e2b --- /dev/null +++ b/src/libgit2/blame.h @@ -0,0 +1,95 @@ +#ifndef INCLUDE_blame_h__ +#define INCLUDE_blame_h__ + +#include "common.h" + +#include "git2/blame.h" +#include "vector.h" +#include "diff.h" +#include "array.h" +#include "git2/oid.h" + +/* + * One blob in a commit that is being suspected + */ +typedef struct git_blame__origin { + int refcnt; + struct git_blame__origin *previous; + git_commit *commit; + git_blob *blob; + char path[GIT_FLEX_ARRAY]; +} git_blame__origin; + +/* + * Each group of lines is described by a git_blame__entry; it can be split + * as we pass blame to the parents. They form a linked list in the + * scoreboard structure, sorted by the target line number. + */ +typedef struct git_blame__entry { + struct git_blame__entry *prev; + struct git_blame__entry *next; + + /* the first line of this group in the final image; + * internally all line numbers are 0 based. + */ + size_t lno; + + /* how many lines this group has */ + size_t num_lines; + + /* the commit that introduced this group into the final image */ + git_blame__origin *suspect; + + /* true if the suspect is truly guilty; false while we have not + * checked if the group came from one of its parents. + */ + bool guilty; + + /* true if the entry has been scanned for copies in the current parent + */ + bool scanned; + + /* the line number of the first line of this group in the + * suspect's file; internally all line numbers are 0 based. + */ + size_t s_lno; + + /* how significant this entry is -- cached to avoid + * scanning the lines over and over. + */ + unsigned score; + + /* Whether this entry has been tracked to a boundary commit. + */ + bool is_boundary; +} git_blame__entry; + +struct git_blame { + char *path; + git_repository *repository; + git_mailmap *mailmap; + git_blame_options options; + + git_vector hunks; + git_vector paths; + + git_blob *final_blob; + git_array_t(size_t) line_index; + + size_t current_diff_line; + git_blame_hunk *current_hunk; + + /* Scoreboard fields */ + git_commit *final; + git_blame__entry *ent; + int num_lines; + const char *final_buf; + size_t final_buf_size; +}; + +git_blame *git_blame__alloc( + git_repository *repo, + git_blame_options opts, + const char *path); + +#endif diff --git a/src/libgit2/blame_git.c b/src/libgit2/blame_git.c new file mode 100644 index 0000000..69897b3 --- /dev/null +++ b/src/libgit2/blame_git.c @@ -0,0 +1,684 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "blame_git.h" + +#include "commit.h" +#include "blob.h" +#include "diff_xdiff.h" + +/* + * Origin is refcounted and usually we keep the blob contents to be + * reused. + */ +static git_blame__origin *origin_incref(git_blame__origin *o) +{ + if (o) + o->refcnt++; + return o; +} + +static void origin_decref(git_blame__origin *o) +{ + if (o && --o->refcnt <= 0) { + if (o->previous) + origin_decref(o->previous); + git_blob_free(o->blob); + git_commit_free(o->commit); + git__free(o); + } +} + +/* Given a commit and a path in it, create a new origin structure. */ +static int make_origin(git_blame__origin **out, git_commit *commit, const char *path) +{ + git_blame__origin *o; + git_object *blob; + size_t path_len = strlen(path), alloc_len; + int error = 0; + + if ((error = git_object_lookup_bypath(&blob, (git_object*)commit, + path, GIT_OBJECT_BLOB)) < 0) + return error; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, sizeof(*o), path_len); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, 1); + o = git__calloc(1, alloc_len); + GIT_ERROR_CHECK_ALLOC(o); + + o->commit = commit; + o->blob = (git_blob *) blob; + o->refcnt = 1; + strcpy(o->path, path); + + *out = o; + + return 0; +} + +/* Locate an existing origin or create a new one. */ +int git_blame__get_origin( + git_blame__origin **out, + git_blame *blame, + git_commit *commit, + const char *path) +{ + git_blame__entry *e; + + for (e = blame->ent; e; e = e->next) { + if (e->suspect->commit == commit && !strcmp(e->suspect->path, path)) { + *out = origin_incref(e->suspect); + } + } + return make_origin(out, commit, path); +} + +typedef struct blame_chunk_cb_data { + git_blame *blame; + git_blame__origin *target; + git_blame__origin *parent; + long tlno; + long plno; +}blame_chunk_cb_data; + +static bool same_suspect(git_blame__origin *a, git_blame__origin *b) +{ + if (a == b) + return true; + if (git_oid_cmp(git_commit_id(a->commit), git_commit_id(b->commit))) + return false; + return 0 == strcmp(a->path, b->path); +} + +/* find the line number of the last line the target is suspected for */ +static bool find_last_in_target(size_t *out, git_blame *blame, git_blame__origin *target) +{ + git_blame__entry *e; + size_t last_in_target = 0; + bool found = false; + + *out = 0; + + for (e=blame->ent; e; e=e->next) { + if (e->guilty || !same_suspect(e->suspect, target)) + continue; + if (last_in_target < e->s_lno + e->num_lines) { + found = true; + last_in_target = e->s_lno + e->num_lines; + } + } + + *out = last_in_target; + return found; +} + +/* + * It is known that lines between tlno to same came from parent, and e + * has an overlap with that range. it also is known that parent's + * line plno corresponds to e's line tlno. + * + * <---- e -----> + * <------> (entirely within) + * <------------> (extends past) + * <------------> (starts before) + * <------------------> (entirely encloses) + * + * Split e into potentially three parts; before this chunk, the chunk + * to be blamed for the parent, and after that portion. + */ +static void split_overlap(git_blame__entry *split, git_blame__entry *e, + size_t tlno, size_t plno, size_t same, git_blame__origin *parent) +{ + size_t chunk_end_lno; + + if (e->s_lno < tlno) { + /* there is a pre-chunk part not blamed on the parent */ + split[0].suspect = origin_incref(e->suspect); + split[0].lno = e->lno; + split[0].s_lno = e->s_lno; + split[0].num_lines = tlno - e->s_lno; + split[1].lno = e->lno + tlno - e->s_lno; + split[1].s_lno = plno; + } else { + split[1].lno = e->lno; + split[1].s_lno = plno + (e->s_lno - tlno); + } + + if (same < e->s_lno + e->num_lines) { + /* there is a post-chunk part not blamed on parent */ + split[2].suspect = origin_incref(e->suspect); + split[2].lno = e->lno + (same - e->s_lno); + split[2].s_lno = e->s_lno + (same - e->s_lno); + split[2].num_lines = e->s_lno + e->num_lines - same; + chunk_end_lno = split[2].lno; + } else { + chunk_end_lno = e->lno + e->num_lines; + } + split[1].num_lines = chunk_end_lno - split[1].lno; + + /* + * if it turns out there is nothing to blame the parent for, forget about + * the splitting. !split[1].suspect signals this. + */ + if (split[1].num_lines < 1) + return; + split[1].suspect = origin_incref(parent); +} + +/* + * Link in a new blame entry to the scoreboard. Entries that cover the same + * line range have been removed from the scoreboard previously. + */ +static void add_blame_entry(git_blame *blame, git_blame__entry *e) +{ + git_blame__entry *ent, *prev = NULL; + + origin_incref(e->suspect); + + for (ent = blame->ent; ent && ent->lno < e->lno; ent = ent->next) + prev = ent; + + /* prev, if not NULL, is the last one that is below e */ + e->prev = prev; + if (prev) { + e->next = prev->next; + prev->next = e; + } else { + e->next = blame->ent; + blame->ent = e; + } + if (e->next) + e->next->prev = e; +} + +/* + * src typically is on-stack; we want to copy the information in it to + * a malloced blame_entry that is already on the linked list of the scoreboard. + * The origin of dst loses a refcnt while the origin of src gains one. + */ +static void dup_entry(git_blame__entry *dst, git_blame__entry *src) +{ + git_blame__entry *p, *n; + + p = dst->prev; + n = dst->next; + origin_incref(src->suspect); + origin_decref(dst->suspect); + memcpy(dst, src, sizeof(*src)); + dst->prev = p; + dst->next = n; + dst->score = 0; +} + +/* + * split_overlap() divided an existing blame e into up to three parts in split. + * Adjust the linked list of blames in the scoreboard to reflect the split. + */ +static int split_blame(git_blame *blame, git_blame__entry *split, git_blame__entry *e) +{ + git_blame__entry *new_entry; + + if (split[0].suspect && split[2].suspect) { + /* The first part (reuse storage for the existing entry e */ + dup_entry(e, &split[0]); + + /* The last part -- me */ + new_entry = git__malloc(sizeof(*new_entry)); + GIT_ERROR_CHECK_ALLOC(new_entry); + memcpy(new_entry, &(split[2]), sizeof(git_blame__entry)); + add_blame_entry(blame, new_entry); + + /* ... and the middle part -- parent */ + new_entry = git__malloc(sizeof(*new_entry)); + GIT_ERROR_CHECK_ALLOC(new_entry); + memcpy(new_entry, &(split[1]), sizeof(git_blame__entry)); + add_blame_entry(blame, new_entry); + } else if (!split[0].suspect && !split[2].suspect) { + /* + * The parent covers the entire area; reuse storage for e and replace it + * with the parent + */ + dup_entry(e, &split[1]); + } else if (split[0].suspect) { + /* me and then parent */ + dup_entry(e, &split[0]); + new_entry = git__malloc(sizeof(*new_entry)); + GIT_ERROR_CHECK_ALLOC(new_entry); + memcpy(new_entry, &(split[1]), sizeof(git_blame__entry)); + add_blame_entry(blame, new_entry); + } else { + /* parent and then me */ + dup_entry(e, &split[1]); + new_entry = git__malloc(sizeof(*new_entry)); + GIT_ERROR_CHECK_ALLOC(new_entry); + memcpy(new_entry, &(split[2]), sizeof(git_blame__entry)); + add_blame_entry(blame, new_entry); + } + + return 0; +} + +/* + * After splitting the blame, the origins used by the on-stack blame_entry + * should lose one refcnt each. + */ +static void decref_split(git_blame__entry *split) +{ + int i; + for (i=0; i<3; i++) + origin_decref(split[i].suspect); +} + +/* + * Helper for blame_chunk(). blame_entry e is known to overlap with the patch + * hunk; split it and pass blame to the parent. + */ +static int blame_overlap( + git_blame *blame, + git_blame__entry *e, + size_t tlno, + size_t plno, + size_t same, + git_blame__origin *parent) +{ + git_blame__entry split[3] = {{0}}; + + split_overlap(split, e, tlno, plno, same, parent); + if (split[1].suspect) + if (split_blame(blame, split, e) < 0) + return -1; + decref_split(split); + + return 0; +} + +/* + * Process one hunk from the patch between the current suspect for blame_entry + * e and its parent. Find and split the overlap, and pass blame to the + * overlapping part to the parent. + */ +static int blame_chunk( + git_blame *blame, + size_t tlno, + size_t plno, + size_t same, + git_blame__origin *target, + git_blame__origin *parent) +{ + git_blame__entry *e; + + for (e = blame->ent; e; e = e->next) { + if (e->guilty || !same_suspect(e->suspect, target)) + continue; + if (same <= e->s_lno) + continue; + if (tlno < e->s_lno + e->num_lines) { + if (blame_overlap(blame, e, tlno, plno, same, parent) < 0) + return -1; + } + } + + return 0; +} + +static int my_emit( + long start_a, long count_a, + long start_b, long count_b, + void *cb_data) +{ + blame_chunk_cb_data *d = (blame_chunk_cb_data *)cb_data; + + if (blame_chunk(d->blame, d->tlno, d->plno, start_b, d->target, d->parent) < 0) + return -1; + d->plno = start_a + count_a; + d->tlno = start_b + count_b; + + return 0; +} + +static void trim_common_tail(mmfile_t *a, mmfile_t *b, long ctx) +{ + const int blk = 1024; + long trimmed = 0, recovered = 0; + char *ap = a->ptr + a->size; + char *bp = b->ptr + b->size; + long smaller = (long)((a->size < b->size) ? a->size : b->size); + + if (ctx) + return; + + while (blk + trimmed <= smaller && !memcmp(ap - blk, bp - blk, blk)) { + trimmed += blk; + ap -= blk; + bp -= blk; + } + + while (recovered < trimmed) + if (ap[recovered++] == '\n') + break; + a->size -= trimmed - recovered; + b->size -= trimmed - recovered; +} + +static int diff_hunks(mmfile_t file_a, mmfile_t file_b, void *cb_data, git_blame_options *options) +{ + xdemitconf_t xecfg = {0}; + xdemitcb_t ecb = {0}; + xpparam_t xpp = {0}; + + if (options->flags & GIT_BLAME_IGNORE_WHITESPACE) + xpp.flags |= XDF_IGNORE_WHITESPACE; + + xecfg.hunk_func = my_emit; + ecb.priv = cb_data; + + trim_common_tail(&file_a, &file_b, 0); + + if (file_a.size > GIT_XDIFF_MAX_SIZE || + file_b.size > GIT_XDIFF_MAX_SIZE) { + git_error_set(GIT_ERROR_INVALID, "file too large to blame"); + return -1; + } + + return xdl_diff(&file_a, &file_b, &xpp, &xecfg, &ecb); +} + +static void fill_origin_blob(git_blame__origin *o, mmfile_t *file) +{ + memset(file, 0, sizeof(*file)); + if (o->blob) { + file->ptr = (char*)git_blob_rawcontent(o->blob); + file->size = (long)git_blob_rawsize(o->blob); + } +} + +static int pass_blame_to_parent( + git_blame *blame, + git_blame__origin *target, + git_blame__origin *parent) +{ + size_t last_in_target; + mmfile_t file_p, file_o; + blame_chunk_cb_data d = { blame, target, parent, 0, 0 }; + + if (!find_last_in_target(&last_in_target, blame, target)) + return 1; /* nothing remains for this target */ + + fill_origin_blob(parent, &file_p); + fill_origin_blob(target, &file_o); + + if (diff_hunks(file_p, file_o, &d, &blame->options) < 0) + return -1; + + /* The reset (i.e. anything after tlno) are the same as the parent */ + if (blame_chunk(blame, d.tlno, d.plno, last_in_target, target, parent) < 0) + return -1; + + return 0; +} + +static int paths_on_dup(void **old, void *new) +{ + GIT_UNUSED(old); + git__free(new); + return -1; +} + +static git_blame__origin *find_origin( + git_blame *blame, + git_commit *parent, + git_blame__origin *origin) +{ + git_blame__origin *porigin = NULL; + git_diff *difflist = NULL; + git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT; + git_tree *otree=NULL, *ptree=NULL; + + /* Get the trees from this commit and its parent */ + if (0 != git_commit_tree(&otree, origin->commit) || + 0 != git_commit_tree(&ptree, parent)) + goto cleanup; + + /* Configure the diff */ + diffopts.context_lines = 0; + diffopts.flags = GIT_DIFF_SKIP_BINARY_CHECK; + + /* Check to see if files we're interested have changed */ + diffopts.pathspec.count = blame->paths.length; + diffopts.pathspec.strings = (char**)blame->paths.contents; + if (0 != git_diff_tree_to_tree(&difflist, blame->repository, ptree, otree, &diffopts)) + goto cleanup; + + if (!git_diff_num_deltas(difflist)) { + /* No changes; copy data */ + git_blame__get_origin(&porigin, blame, parent, origin->path); + } else { + git_diff_find_options findopts = GIT_DIFF_FIND_OPTIONS_INIT; + int i; + + /* Generate a full diff between the two trees */ + git_diff_free(difflist); + diffopts.pathspec.count = 0; + if (0 != git_diff_tree_to_tree(&difflist, blame->repository, ptree, otree, &diffopts)) + goto cleanup; + + /* Let diff find renames */ + findopts.flags = GIT_DIFF_FIND_RENAMES; + if (0 != git_diff_find_similar(difflist, &findopts)) + goto cleanup; + + /* Find one that matches */ + for (i=0; i<(int)git_diff_num_deltas(difflist); i++) { + const git_diff_delta *delta = git_diff_get_delta(difflist, i); + + if (!git_vector_bsearch(NULL, &blame->paths, delta->new_file.path)) + { + git_vector_insert_sorted(&blame->paths, (void*)git__strdup(delta->old_file.path), + paths_on_dup); + make_origin(&porigin, parent, delta->old_file.path); + } + } + } + +cleanup: + git_diff_free(difflist); + git_tree_free(otree); + git_tree_free(ptree); + return porigin; +} + +/* + * The blobs of origin and porigin exactly match, so everything origin is + * suspected for can be blamed on the parent. + */ +static int pass_whole_blame(git_blame *blame, + git_blame__origin *origin, git_blame__origin *porigin) +{ + git_blame__entry *e; + + if (!porigin->blob && + git_object_lookup((git_object**)&porigin->blob, blame->repository, + git_blob_id(origin->blob), GIT_OBJECT_BLOB) < 0) + return -1; + for (e=blame->ent; e; e=e->next) { + if (!same_suspect(e->suspect, origin)) + continue; + origin_incref(porigin); + origin_decref(e->suspect); + e->suspect = porigin; + } + + return 0; +} + +static int pass_blame(git_blame *blame, git_blame__origin *origin, uint32_t opt) +{ + git_commit *commit = origin->commit; + int i, num_parents; + git_blame__origin *sg_buf[16]; + git_blame__origin *porigin, **sg_origin = sg_buf; + int ret, error = 0; + + num_parents = git_commit_parentcount(commit); + if (!git_oid_cmp(git_commit_id(commit), &blame->options.oldest_commit)) + /* Stop at oldest specified commit */ + num_parents = 0; + else if (opt & GIT_BLAME_FIRST_PARENT && num_parents > 1) + /* Limit search to the first parent */ + num_parents = 1; + + if (!num_parents) { + git_oid_cpy(&blame->options.oldest_commit, git_commit_id(commit)); + goto finish; + } else if (num_parents < (int)ARRAY_SIZE(sg_buf)) + memset(sg_buf, 0, sizeof(sg_buf)); + else { + sg_origin = git__calloc(num_parents, sizeof(*sg_origin)); + GIT_ERROR_CHECK_ALLOC(sg_origin); + } + + for (i=0; icommit, i)) < 0) + goto finish; + porigin = find_origin(blame, p, origin); + + if (!porigin) { + /* + * We only have to decrement the parent's + * reference count when no porigin has + * been created, as otherwise the commit + * is assigned to the created object. + */ + git_commit_free(p); + continue; + } + if (porigin->blob && origin->blob && + !git_oid_cmp(git_blob_id(porigin->blob), git_blob_id(origin->blob))) { + error = pass_whole_blame(blame, origin, porigin); + origin_decref(porigin); + goto finish; + } + for (j = same = 0; jblob), git_blob_id(porigin->blob))) { + same = 1; + break; + } + if (!same) + sg_origin[i] = porigin; + else + origin_decref(porigin); + } + + /* Standard blame */ + for (i=0; iprevious) { + origin_incref(porigin); + origin->previous = porigin; + } + + if ((ret = pass_blame_to_parent(blame, origin, porigin)) != 0) { + if (ret < 0) + error = -1; + + goto finish; + } + } + + /* TODO: optionally find moves in parents' files */ + + /* TODO: optionally find copies in parents' files */ + +finish: + for (i=0; i pair), + * merge them together. + */ +static void coalesce(git_blame *blame) +{ + git_blame__entry *ent, *next; + + for (ent=blame->ent; ent && (next = ent->next); ent = next) { + if (same_suspect(ent->suspect, next->suspect) && + ent->guilty == next->guilty && + ent->s_lno + ent->num_lines == next->s_lno) + { + ent->num_lines += next->num_lines; + ent->next = next->next; + if (ent->next) + ent->next->prev = ent; + origin_decref(next->suspect); + git__free(next); + ent->score = 0; + next = ent; /* again */ + } + } +} + +int git_blame__like_git(git_blame *blame, uint32_t opt) +{ + int error = 0; + + while (true) { + git_blame__entry *ent; + git_blame__origin *suspect = NULL; + + /* Find a suspect to break down */ + for (ent = blame->ent; !suspect && ent; ent = ent->next) + if (!ent->guilty) + suspect = ent->suspect; + if (!suspect) + break; + + /* We'll use this suspect later in the loop, so hold on to it for now. */ + origin_incref(suspect); + + if ((error = pass_blame(blame, suspect, opt)) < 0) + break; + + /* Take responsibility for the remaining entries */ + for (ent = blame->ent; ent; ent = ent->next) { + if (same_suspect(ent->suspect, suspect)) { + ent->guilty = true; + ent->is_boundary = !git_oid_cmp( + git_commit_id(suspect->commit), + &blame->options.oldest_commit); + } + } + origin_decref(suspect); + } + + if (!error) + coalesce(blame); + + return error; +} + +void git_blame__free_entry(git_blame__entry *ent) +{ + if (!ent) return; + origin_decref(ent->suspect); + git__free(ent); +} diff --git a/src/libgit2/blame_git.h b/src/libgit2/blame_git.h new file mode 100644 index 0000000..48b85a2 --- /dev/null +++ b/src/libgit2/blame_git.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_blame_git__ +#define INCLUDE_blame_git__ + +#include "common.h" + +#include "blame.h" + +int git_blame__get_origin( + git_blame__origin **out, + git_blame *sb, + git_commit *commit, + const char *path); +void git_blame__free_entry(git_blame__entry *ent); +int git_blame__like_git(git_blame *sb, uint32_t flags); + +#endif diff --git a/src/libgit2/blob.c b/src/libgit2/blob.c new file mode 100644 index 0000000..5cfd747 --- /dev/null +++ b/src/libgit2/blob.c @@ -0,0 +1,530 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "blob.h" + +#include "git2/common.h" +#include "git2/object.h" +#include "git2/repository.h" +#include "git2/odb_backend.h" + +#include "buf.h" +#include "filebuf.h" +#include "filter.h" + +const void *git_blob_rawcontent(const git_blob *blob) +{ + GIT_ASSERT_ARG_WITH_RETVAL(blob, NULL); + + if (blob->raw) + return blob->data.raw.data; + else + return git_odb_object_data(blob->data.odb); +} + +git_object_size_t git_blob_rawsize(const git_blob *blob) +{ + GIT_ASSERT_ARG(blob); + + if (blob->raw) + return blob->data.raw.size; + else + return (git_object_size_t)git_odb_object_size(blob->data.odb); +} + +int git_blob__getbuf(git_str *buffer, git_blob *blob) +{ + git_object_size_t size = git_blob_rawsize(blob); + + GIT_ERROR_CHECK_BLOBSIZE(size); + return git_str_set(buffer, git_blob_rawcontent(blob), (size_t)size); +} + +void git_blob__free(void *_blob) +{ + git_blob *blob = (git_blob *) _blob; + if (!blob->raw) + git_odb_object_free(blob->data.odb); + git__free(blob); +} + +int git_blob__parse_raw(void *_blob, const char *data, size_t size, git_oid_t oid_type) +{ + git_blob *blob = (git_blob *) _blob; + + GIT_ASSERT_ARG(blob); + GIT_UNUSED(oid_type); + + blob->raw = 1; + blob->data.raw.data = data; + blob->data.raw.size = size; + return 0; +} + +int git_blob__parse(void *_blob, git_odb_object *odb_obj, git_oid_t oid_type) +{ + git_blob *blob = (git_blob *) _blob; + + GIT_ASSERT_ARG(blob); + GIT_UNUSED(oid_type); + + git_cached_obj_incref((git_cached_obj *)odb_obj); + blob->raw = 0; + blob->data.odb = odb_obj; + return 0; +} + +int git_blob_create_from_buffer( + git_oid *id, git_repository *repo, const void *buffer, size_t len) +{ + int error; + git_odb *odb; + git_odb_stream *stream; + + GIT_ASSERT_ARG(id); + GIT_ASSERT_ARG(repo); + + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0 || + (error = git_odb_open_wstream(&stream, odb, len, GIT_OBJECT_BLOB)) < 0) + return error; + + if ((error = git_odb_stream_write(stream, buffer, len)) == 0) + error = git_odb_stream_finalize_write(id, stream); + + git_odb_stream_free(stream); + return error; +} + +static int write_file_stream( + git_oid *id, git_odb *odb, const char *path, git_object_size_t file_size) +{ + int fd, error; + char buffer[GIT_BUFSIZE_FILEIO]; + git_odb_stream *stream = NULL; + ssize_t read_len = -1; + git_object_size_t written = 0; + + if ((error = git_odb_open_wstream( + &stream, odb, file_size, GIT_OBJECT_BLOB)) < 0) + return error; + + if ((fd = git_futils_open_ro(path)) < 0) { + git_odb_stream_free(stream); + return -1; + } + + while (!error && (read_len = p_read(fd, buffer, sizeof(buffer))) > 0) { + error = git_odb_stream_write(stream, buffer, read_len); + written += read_len; + } + + p_close(fd); + + if (written != file_size || read_len < 0) { + git_error_set(GIT_ERROR_OS, "failed to read file into stream"); + error = -1; + } + + if (!error) + error = git_odb_stream_finalize_write(id, stream); + + git_odb_stream_free(stream); + return error; +} + +static int write_file_filtered( + git_oid *id, + git_object_size_t *size, + git_odb *odb, + const char *full_path, + git_filter_list *fl, + git_repository* repo) +{ + int error; + git_str tgt = GIT_STR_INIT; + + error = git_filter_list__apply_to_file(&tgt, fl, repo, full_path); + + /* Write the file to disk if it was properly filtered */ + if (!error) { + *size = tgt.size; + + error = git_odb_write(id, odb, tgt.ptr, tgt.size, GIT_OBJECT_BLOB); + } + + git_str_dispose(&tgt); + return error; +} + +static int write_symlink( + git_oid *id, git_odb *odb, const char *path, size_t link_size) +{ + char *link_data; + ssize_t read_len; + int error; + + link_data = git__malloc(link_size); + GIT_ERROR_CHECK_ALLOC(link_data); + + read_len = p_readlink(path, link_data, link_size); + if (read_len != (ssize_t)link_size) { + git_error_set(GIT_ERROR_OS, "failed to create blob: cannot read symlink '%s'", path); + git__free(link_data); + return -1; + } + + error = git_odb_write(id, odb, (void *)link_data, link_size, GIT_OBJECT_BLOB); + git__free(link_data); + return error; +} + +int git_blob__create_from_paths( + git_oid *id, + struct stat *out_st, + git_repository *repo, + const char *content_path, + const char *hint_path, + mode_t hint_mode, + bool try_load_filters) +{ + int error; + struct stat st; + git_odb *odb = NULL; + git_object_size_t size; + mode_t mode; + git_str path = GIT_STR_INIT; + + GIT_ASSERT_ARG(hint_path || !try_load_filters); + + if (!content_path) { + if (git_repository_workdir_path(&path, repo, hint_path) < 0) + return -1; + + content_path = path.ptr; + } + + if ((error = git_fs_path_lstat(content_path, &st)) < 0 || + (error = git_repository_odb(&odb, repo)) < 0) + goto done; + + if (S_ISDIR(st.st_mode)) { + git_error_set(GIT_ERROR_ODB, "cannot create blob from '%s': it is a directory", content_path); + error = GIT_EDIRECTORY; + goto done; + } + + if (out_st) + memcpy(out_st, &st, sizeof(st)); + + size = st.st_size; + mode = hint_mode ? hint_mode : st.st_mode; + + if (S_ISLNK(mode)) { + error = write_symlink(id, odb, content_path, (size_t)size); + } else { + git_filter_list *fl = NULL; + + if (try_load_filters) + /* Load the filters for writing this file to the ODB */ + error = git_filter_list_load( + &fl, repo, NULL, hint_path, + GIT_FILTER_TO_ODB, GIT_FILTER_DEFAULT); + + if (error < 0) + /* well, that didn't work */; + else if (fl == NULL) + /* No filters need to be applied to the document: we can stream + * directly from disk */ + error = write_file_stream(id, odb, content_path, size); + else { + /* We need to apply one or more filters */ + error = write_file_filtered(id, &size, odb, content_path, fl, repo); + + git_filter_list_free(fl); + } + + /* + * TODO: eventually support streaming filtered files, for files + * which are bigger than a given threshold. This is not a priority + * because applying a filter in streaming mode changes the final + * size of the blob, and without knowing its final size, the blob + * cannot be written in stream mode to the ODB. + * + * The plan is to do streaming writes to a tempfile on disk and then + * opening streaming that file to the ODB, using + * `write_file_stream`. + * + * CAREFULLY DESIGNED APIS YO + */ + } + +done: + git_odb_free(odb); + git_str_dispose(&path); + + return error; +} + +int git_blob_create_from_workdir( + git_oid *id, git_repository *repo, const char *path) +{ + return git_blob__create_from_paths(id, NULL, repo, NULL, path, 0, true); +} + +int git_blob_create_from_disk( + git_oid *id, git_repository *repo, const char *path) +{ + int error; + git_str full_path = GIT_STR_INIT; + const char *workdir, *hintpath = NULL; + + if ((error = git_fs_path_prettify(&full_path, path, NULL)) < 0) { + git_str_dispose(&full_path); + return error; + } + + workdir = git_repository_workdir(repo); + + if (workdir && !git__prefixcmp(full_path.ptr, workdir)) + hintpath = full_path.ptr + strlen(workdir); + + error = git_blob__create_from_paths( + id, NULL, repo, git_str_cstr(&full_path), hintpath, 0, !!hintpath); + + git_str_dispose(&full_path); + return error; +} + +typedef struct { + git_writestream parent; + git_filebuf fbuf; + git_repository *repo; + char *hintpath; +} blob_writestream; + +static int blob_writestream_close(git_writestream *_stream) +{ + blob_writestream *stream = (blob_writestream *) _stream; + + git_filebuf_cleanup(&stream->fbuf); + return 0; +} + +static void blob_writestream_free(git_writestream *_stream) +{ + blob_writestream *stream = (blob_writestream *) _stream; + + git_filebuf_cleanup(&stream->fbuf); + git__free(stream->hintpath); + git__free(stream); +} + +static int blob_writestream_write(git_writestream *_stream, const char *buffer, size_t len) +{ + blob_writestream *stream = (blob_writestream *) _stream; + + return git_filebuf_write(&stream->fbuf, buffer, len); +} + +int git_blob_create_from_stream(git_writestream **out, git_repository *repo, const char *hintpath) +{ + int error; + git_str path = GIT_STR_INIT; + blob_writestream *stream; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + stream = git__calloc(1, sizeof(blob_writestream)); + GIT_ERROR_CHECK_ALLOC(stream); + + if (hintpath) { + stream->hintpath = git__strdup(hintpath); + GIT_ERROR_CHECK_ALLOC(stream->hintpath); + } + + stream->repo = repo; + stream->parent.write = blob_writestream_write; + stream->parent.close = blob_writestream_close; + stream->parent.free = blob_writestream_free; + + if ((error = git_repository__item_path(&path, repo, GIT_REPOSITORY_ITEM_OBJECTS)) < 0 + || (error = git_str_joinpath(&path, path.ptr, "streamed")) < 0) + goto cleanup; + + if ((error = git_filebuf_open_withsize(&stream->fbuf, git_str_cstr(&path), GIT_FILEBUF_TEMPORARY, + 0666, 2 * 1024 * 1024)) < 0) + goto cleanup; + + *out = (git_writestream *) stream; + +cleanup: + if (error < 0) + blob_writestream_free((git_writestream *) stream); + + git_str_dispose(&path); + return error; +} + +int git_blob_create_from_stream_commit(git_oid *out, git_writestream *_stream) +{ + int error; + blob_writestream *stream = (blob_writestream *) _stream; + + /* + * We can make this more officient by avoiding writing to + * disk, but for now let's re-use the helper functions we + * have. + */ + if ((error = git_filebuf_flush(&stream->fbuf)) < 0) + goto cleanup; + + error = git_blob__create_from_paths(out, NULL, stream->repo, stream->fbuf.path_lock, + stream->hintpath, 0, !!stream->hintpath); + +cleanup: + blob_writestream_free(_stream); + return error; + +} + +int git_blob_is_binary(const git_blob *blob) +{ + git_str content = GIT_STR_INIT; + git_object_size_t size; + + GIT_ASSERT_ARG(blob); + + size = git_blob_rawsize(blob); + + git_str_attach_notowned(&content, git_blob_rawcontent(blob), + (size_t)min(size, GIT_FILTER_BYTES_TO_CHECK_NUL)); + return git_str_is_binary(&content); +} + +int git_blob_data_is_binary(const char *str, size_t len) +{ + git_str content = GIT_STR_INIT; + + git_str_attach_notowned(&content, str, len); + + return git_str_is_binary(&content); +} + +int git_blob_filter_options_init( + git_blob_filter_options *opts, + unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE(opts, version, + git_blob_filter_options, GIT_BLOB_FILTER_OPTIONS_INIT); + return 0; +} + +int git_blob_filter( + git_buf *out, + git_blob *blob, + const char *path, + git_blob_filter_options *given_opts) +{ + git_blob_filter_options opts = GIT_BLOB_FILTER_OPTIONS_INIT; + git_filter_options filter_opts = GIT_FILTER_OPTIONS_INIT; + git_filter_list *fl = NULL; + int error = 0; + + GIT_ASSERT_ARG(blob); + GIT_ASSERT_ARG(path); + GIT_ASSERT_ARG(out); + + GIT_ERROR_CHECK_VERSION( + given_opts, GIT_BLOB_FILTER_OPTIONS_VERSION, "git_blob_filter_options"); + + if (given_opts != NULL) + memcpy(&opts, given_opts, sizeof(git_blob_filter_options)); + + if ((opts.flags & GIT_BLOB_FILTER_CHECK_FOR_BINARY) != 0 && + git_blob_is_binary(blob)) + return 0; + + if ((opts.flags & GIT_BLOB_FILTER_NO_SYSTEM_ATTRIBUTES) != 0) + filter_opts.flags |= GIT_FILTER_NO_SYSTEM_ATTRIBUTES; + + if ((opts.flags & GIT_BLOB_FILTER_ATTRIBUTES_FROM_HEAD) != 0) + filter_opts.flags |= GIT_FILTER_ATTRIBUTES_FROM_HEAD; + + if ((opts.flags & GIT_BLOB_FILTER_ATTRIBUTES_FROM_COMMIT) != 0) { + filter_opts.flags |= GIT_FILTER_ATTRIBUTES_FROM_COMMIT; + +#ifndef GIT_DEPRECATE_HARD + if (opts.commit_id) + git_oid_cpy(&filter_opts.attr_commit_id, opts.commit_id); + else +#endif + git_oid_cpy(&filter_opts.attr_commit_id, &opts.attr_commit_id); + } + + if (!(error = git_filter_list_load_ext( + &fl, git_blob_owner(blob), blob, path, + GIT_FILTER_TO_WORKTREE, &filter_opts))) { + + error = git_filter_list_apply_to_blob(out, fl, blob); + + git_filter_list_free(fl); + } + + return error; +} + +/* Deprecated functions */ + +#ifndef GIT_DEPRECATE_HARD +int git_blob_create_frombuffer( + git_oid *id, git_repository *repo, const void *buffer, size_t len) +{ + return git_blob_create_from_buffer(id, repo, buffer, len); +} + +int git_blob_create_fromworkdir(git_oid *id, git_repository *repo, const char *relative_path) +{ + return git_blob_create_from_workdir(id, repo, relative_path); +} + +int git_blob_create_fromdisk(git_oid *id, git_repository *repo, const char *path) +{ + return git_blob_create_from_disk(id, repo, path); +} + +int git_blob_create_fromstream( + git_writestream **out, + git_repository *repo, + const char *hintpath) +{ + return git_blob_create_from_stream(out, repo, hintpath); +} + +int git_blob_create_fromstream_commit( + git_oid *out, + git_writestream *stream) +{ + return git_blob_create_from_stream_commit(out, stream); +} + +int git_blob_filtered_content( + git_buf *out, + git_blob *blob, + const char *path, + int check_for_binary_data) +{ + git_blob_filter_options opts = GIT_BLOB_FILTER_OPTIONS_INIT; + + if (check_for_binary_data) + opts.flags |= GIT_BLOB_FILTER_CHECK_FOR_BINARY; + else + opts.flags &= ~GIT_BLOB_FILTER_CHECK_FOR_BINARY; + + return git_blob_filter(out, blob, path, &opts); +} +#endif diff --git a/src/libgit2/blob.h b/src/libgit2/blob.h new file mode 100644 index 0000000..d6c9dd9 --- /dev/null +++ b/src/libgit2/blob.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_blob_h__ +#define INCLUDE_blob_h__ + +#include "common.h" + +#include "git2/blob.h" +#include "repository.h" +#include "odb.h" +#include "futils.h" + +struct git_blob { + git_object object; + + union { + git_odb_object *odb; + struct { + const char *data; + git_object_size_t size; + } raw; + } data; + unsigned int raw:1; +}; + +#define GIT_ERROR_CHECK_BLOBSIZE(n) \ + do { \ + if (!git__is_sizet(n)) { \ + git_error_set(GIT_ERROR_NOMEMORY, "blob contents too large to fit in memory"); \ + return -1; \ + } \ + } while(0) + +void git_blob__free(void *blob); +int git_blob__parse(void *blob, git_odb_object *obj, git_oid_t oid_type); +int git_blob__parse_raw(void *blob, const char *data, size_t size, git_oid_t oid_type); +int git_blob__getbuf(git_str *buffer, git_blob *blob); + +extern int git_blob__create_from_paths( + git_oid *out_oid, + struct stat *out_st, + git_repository *repo, + const char *full_path, + const char *hint_path, + mode_t hint_mode, + bool apply_filters); + +#endif diff --git a/src/libgit2/branch.c b/src/libgit2/branch.c new file mode 100644 index 0000000..9a31c9c --- /dev/null +++ b/src/libgit2/branch.c @@ -0,0 +1,823 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "branch.h" + +#include "buf.h" +#include "commit.h" +#include "tag.h" +#include "config.h" +#include "refspec.h" +#include "refs.h" +#include "remote.h" +#include "annotated_commit.h" +#include "worktree.h" + +#include "git2/branch.h" + +static int retrieve_branch_reference( + git_reference **branch_reference_out, + git_repository *repo, + const char *branch_name, + bool is_remote) +{ + git_reference *branch = NULL; + int error = 0; + char *prefix; + git_str ref_name = GIT_STR_INIT; + + prefix = is_remote ? GIT_REFS_REMOTES_DIR : GIT_REFS_HEADS_DIR; + + if ((error = git_str_joinpath(&ref_name, prefix, branch_name)) < 0) + /* OOM */; + else if ((error = git_reference_lookup(&branch, repo, ref_name.ptr)) < 0) + git_error_set( + GIT_ERROR_REFERENCE, "cannot locate %s branch '%s'", + is_remote ? "remote-tracking" : "local", branch_name); + + *branch_reference_out = branch; /* will be NULL on error */ + + git_str_dispose(&ref_name); + return error; +} + +static int not_a_local_branch(const char *reference_name) +{ + git_error_set( + GIT_ERROR_INVALID, + "reference '%s' is not a local branch.", reference_name); + return -1; +} + +static bool branch_name_is_valid(const char *branch_name) +{ + /* + * Discourage branch name starting with dash, + * https://github.com/git/git/commit/6348624010888b + * and discourage HEAD as branch name, + * https://github.com/git/git/commit/a625b092cc5994 + */ + return branch_name[0] != '-' && git__strcmp(branch_name, "HEAD"); +} + +static int create_branch( + git_reference **ref_out, + git_repository *repository, + const char *branch_name, + const git_commit *commit, + const char *from, + int force) +{ + int is_unmovable_head = 0; + git_reference *branch = NULL; + git_str canonical_branch_name = GIT_STR_INIT, + log_message = GIT_STR_INIT; + int error = -1; + int bare = git_repository_is_bare(repository); + + GIT_ASSERT_ARG(branch_name); + GIT_ASSERT_ARG(commit); + GIT_ASSERT_ARG(ref_out); + GIT_ASSERT_ARG(git_commit_owner(commit) == repository); + + if (!branch_name_is_valid(branch_name)) { + git_error_set(GIT_ERROR_REFERENCE, "'%s' is not a valid branch name", branch_name); + error = -1; + goto cleanup; + } + + if (force && !bare && git_branch_lookup(&branch, repository, branch_name, GIT_BRANCH_LOCAL) == 0) { + error = git_branch_is_head(branch); + git_reference_free(branch); + branch = NULL; + + if (error < 0) + goto cleanup; + + is_unmovable_head = error; + } + + if (is_unmovable_head && force) { + git_error_set(GIT_ERROR_REFERENCE, "cannot force update branch '%s' as it is " + "the current HEAD of the repository.", branch_name); + error = -1; + goto cleanup; + } + + if (git_str_joinpath(&canonical_branch_name, GIT_REFS_HEADS_DIR, branch_name) < 0) + goto cleanup; + + if (git_str_printf(&log_message, "branch: Created from %s", from) < 0) + goto cleanup; + + error = git_reference_create(&branch, repository, + git_str_cstr(&canonical_branch_name), git_commit_id(commit), force, + git_str_cstr(&log_message)); + + if (!error) + *ref_out = branch; + +cleanup: + git_str_dispose(&canonical_branch_name); + git_str_dispose(&log_message); + return error; +} + +int git_branch_create( + git_reference **ref_out, + git_repository *repository, + const char *branch_name, + const git_commit *commit, + int force) +{ + char commit_id[GIT_OID_MAX_HEXSIZE + 1]; + + git_oid_tostr(commit_id, GIT_OID_MAX_HEXSIZE + 1, git_commit_id(commit)); + return create_branch(ref_out, repository, branch_name, commit, commit_id, force); +} + +int git_branch_create_from_annotated( + git_reference **ref_out, + git_repository *repository, + const char *branch_name, + const git_annotated_commit *commit, + int force) +{ + return create_branch(ref_out, + repository, branch_name, commit->commit, commit->description, force); +} + +static int branch_is_checked_out(git_repository *worktree, void *payload) +{ + git_reference *branch = (git_reference *) payload; + git_reference *head = NULL; + int error; + + if (git_repository_is_bare(worktree)) + return 0; + + if ((error = git_reference_lookup(&head, worktree, GIT_HEAD_FILE)) < 0) { + if (error == GIT_ENOTFOUND) + error = 0; + goto out; + } + + if (git_reference_type(head) != GIT_REFERENCE_SYMBOLIC) + goto out; + + error = !git__strcmp(head->target.symbolic, branch->name); + +out: + git_reference_free(head); + return error; +} + +int git_branch_is_checked_out(const git_reference *branch) +{ + GIT_ASSERT_ARG(branch); + + if (!git_reference_is_branch(branch)) + return 0; + return git_repository_foreach_worktree(git_reference_owner(branch), + branch_is_checked_out, (void *)branch) == 1; +} + +int git_branch_delete(git_reference *branch) +{ + int is_head; + git_str config_section = GIT_STR_INIT; + int error = -1; + + GIT_ASSERT_ARG(branch); + + if (!git_reference_is_branch(branch) && !git_reference_is_remote(branch)) { + git_error_set(GIT_ERROR_INVALID, "reference '%s' is not a valid branch.", + git_reference_name(branch)); + return GIT_ENOTFOUND; + } + + if ((is_head = git_branch_is_head(branch)) < 0) + return is_head; + + if (is_head) { + git_error_set(GIT_ERROR_REFERENCE, "cannot delete branch '%s' as it is " + "the current HEAD of the repository.", git_reference_name(branch)); + return -1; + } + + if (git_reference_is_branch(branch) && git_branch_is_checked_out(branch)) { + git_error_set(GIT_ERROR_REFERENCE, "Cannot delete branch '%s' as it is " + "the current HEAD of a linked repository.", git_reference_name(branch)); + return -1; + } + + if (git_str_join(&config_section, '.', "branch", + git_reference_name(branch) + strlen(GIT_REFS_HEADS_DIR)) < 0) + goto on_error; + + if (git_config_rename_section( + git_reference_owner(branch), git_str_cstr(&config_section), NULL) < 0) + goto on_error; + + error = git_reference_delete(branch); + +on_error: + git_str_dispose(&config_section); + return error; +} + +typedef struct { + git_reference_iterator *iter; + unsigned int flags; +} branch_iter; + +int git_branch_next(git_reference **out, git_branch_t *out_type, git_branch_iterator *_iter) +{ + branch_iter *iter = (branch_iter *) _iter; + git_reference *ref; + int error; + + while ((error = git_reference_next(&ref, iter->iter)) == 0) { + if ((iter->flags & GIT_BRANCH_LOCAL) && + !git__prefixcmp(ref->name, GIT_REFS_HEADS_DIR)) { + *out = ref; + *out_type = GIT_BRANCH_LOCAL; + + return 0; + } else if ((iter->flags & GIT_BRANCH_REMOTE) && + !git__prefixcmp(ref->name, GIT_REFS_REMOTES_DIR)) { + *out = ref; + *out_type = GIT_BRANCH_REMOTE; + + return 0; + } else { + git_reference_free(ref); + } + } + + return error; +} + +int git_branch_iterator_new( + git_branch_iterator **out, + git_repository *repo, + git_branch_t list_flags) +{ + branch_iter *iter; + + iter = git__calloc(1, sizeof(branch_iter)); + GIT_ERROR_CHECK_ALLOC(iter); + + iter->flags = list_flags; + + if (git_reference_iterator_new(&iter->iter, repo) < 0) { + git__free(iter); + return -1; + } + + *out = (git_branch_iterator *) iter; + + return 0; +} + +void git_branch_iterator_free(git_branch_iterator *_iter) +{ + branch_iter *iter = (branch_iter *) _iter; + + if (iter == NULL) + return; + + git_reference_iterator_free(iter->iter); + git__free(iter); +} + +int git_branch_move( + git_reference **out, + git_reference *branch, + const char *new_branch_name, + int force) +{ + git_str new_reference_name = GIT_STR_INIT, + old_config_section = GIT_STR_INIT, + new_config_section = GIT_STR_INIT, + log_message = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(branch); + GIT_ASSERT_ARG(new_branch_name); + + if (!git_reference_is_branch(branch)) + return not_a_local_branch(git_reference_name(branch)); + + if ((error = git_str_joinpath(&new_reference_name, GIT_REFS_HEADS_DIR, new_branch_name)) < 0) + goto done; + + if ((error = git_str_printf(&log_message, "branch: renamed %s to %s", + git_reference_name(branch), git_str_cstr(&new_reference_name))) < 0) + goto done; + + /* first update ref then config so failure won't trash config */ + + error = git_reference_rename( + out, branch, git_str_cstr(&new_reference_name), force, + git_str_cstr(&log_message)); + if (error < 0) + goto done; + + git_str_join(&old_config_section, '.', "branch", + git_reference_name(branch) + strlen(GIT_REFS_HEADS_DIR)); + git_str_join(&new_config_section, '.', "branch", new_branch_name); + + error = git_config_rename_section( + git_reference_owner(branch), + git_str_cstr(&old_config_section), + git_str_cstr(&new_config_section)); + +done: + git_str_dispose(&new_reference_name); + git_str_dispose(&old_config_section); + git_str_dispose(&new_config_section); + git_str_dispose(&log_message); + + return error; +} + +int git_branch_lookup( + git_reference **ref_out, + git_repository *repo, + const char *branch_name, + git_branch_t branch_type) +{ + int error = -1; + + GIT_ASSERT_ARG(ref_out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(branch_name); + + switch (branch_type) { + case GIT_BRANCH_LOCAL: + case GIT_BRANCH_REMOTE: + error = retrieve_branch_reference(ref_out, repo, branch_name, branch_type == GIT_BRANCH_REMOTE); + break; + case GIT_BRANCH_ALL: + error = retrieve_branch_reference(ref_out, repo, branch_name, false); + if (error == GIT_ENOTFOUND) + error = retrieve_branch_reference(ref_out, repo, branch_name, true); + break; + default: + GIT_ASSERT(false); + } + return error; +} + +int git_branch_name( + const char **out, + const git_reference *ref) +{ + const char *branch_name; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(ref); + + branch_name = ref->name; + + if (git_reference_is_branch(ref)) { + branch_name += strlen(GIT_REFS_HEADS_DIR); + } else if (git_reference_is_remote(ref)) { + branch_name += strlen(GIT_REFS_REMOTES_DIR); + } else { + git_error_set(GIT_ERROR_INVALID, + "reference '%s' is neither a local nor a remote branch.", ref->name); + return -1; + } + *out = branch_name; + return 0; +} + +static int retrieve_upstream_configuration( + git_str *out, + const git_config *config, + const char *canonical_branch_name, + const char *format) +{ + git_str buf = GIT_STR_INIT; + int error; + + if (git_str_printf(&buf, format, + canonical_branch_name + strlen(GIT_REFS_HEADS_DIR)) < 0) + return -1; + + error = git_config__get_string_buf(out, config, git_str_cstr(&buf)); + git_str_dispose(&buf); + return error; +} + +int git_branch_upstream_name( + git_buf *out, + git_repository *repo, + const char *refname) +{ + GIT_BUF_WRAP_PRIVATE(out, git_branch__upstream_name, repo, refname); +} + +int git_branch__upstream_name( + git_str *out, + git_repository *repo, + const char *refname) +{ + git_str remote_name = GIT_STR_INIT; + git_str merge_name = GIT_STR_INIT; + git_str buf = GIT_STR_INIT; + int error = -1; + git_remote *remote = NULL; + const git_refspec *refspec; + git_config *config; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(refname); + + if (!git_reference__is_branch(refname)) + return not_a_local_branch(refname); + + if ((error = git_repository_config_snapshot(&config, repo)) < 0) + return error; + + if ((error = retrieve_upstream_configuration( + &remote_name, config, refname, "branch.%s.remote")) < 0) + goto cleanup; + + if ((error = retrieve_upstream_configuration( + &merge_name, config, refname, "branch.%s.merge")) < 0) + goto cleanup; + + if (git_str_len(&remote_name) == 0 || git_str_len(&merge_name) == 0) { + git_error_set(GIT_ERROR_REFERENCE, + "branch '%s' does not have an upstream", refname); + error = GIT_ENOTFOUND; + goto cleanup; + } + + if (strcmp(".", git_str_cstr(&remote_name)) != 0) { + if ((error = git_remote_lookup(&remote, repo, git_str_cstr(&remote_name))) < 0) + goto cleanup; + + refspec = git_remote__matching_refspec(remote, git_str_cstr(&merge_name)); + if (!refspec) { + error = GIT_ENOTFOUND; + goto cleanup; + } + + if (git_refspec__transform(&buf, refspec, git_str_cstr(&merge_name)) < 0) + goto cleanup; + } else + if (git_str_set(&buf, git_str_cstr(&merge_name), git_str_len(&merge_name)) < 0) + goto cleanup; + + git_str_swap(out, &buf); + +cleanup: + git_config_free(config); + git_remote_free(remote); + git_str_dispose(&remote_name); + git_str_dispose(&merge_name); + git_str_dispose(&buf); + return error; +} + +static int git_branch_upstream_with_format( + git_str *out, + git_repository *repo, + const char *refname, + const char *format, + const char *format_name) +{ + git_config *cfg; + int error; + + if (!git_reference__is_branch(refname)) + return not_a_local_branch(refname); + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0 || + (error = retrieve_upstream_configuration(out, cfg, refname, format)) < 0) + return error; + + if (git_str_len(out) == 0) { + git_error_set(GIT_ERROR_REFERENCE, "branch '%s' does not have an upstream %s", refname, format_name); + error = GIT_ENOTFOUND; + } + + return error; +} + +int git_branch_upstream_remote( + git_buf *out, + git_repository *repo, + const char *refname) +{ + GIT_BUF_WRAP_PRIVATE(out, git_branch__upstream_remote, repo, refname); +} + +int git_branch__upstream_remote( + git_str *out, + git_repository *repo, + const char *refname) +{ + return git_branch_upstream_with_format(out, repo, refname, "branch.%s.remote", "remote"); +} + +int git_branch_upstream_merge( + git_buf *out, + git_repository *repo, + const char *refname) +{ + GIT_BUF_WRAP_PRIVATE(out, git_branch__upstream_merge, repo, refname); +} + +int git_branch__upstream_merge( + git_str *out, + git_repository *repo, + const char *refname) +{ + return git_branch_upstream_with_format(out, repo, refname, "branch.%s.merge", "merge"); +} + +int git_branch_remote_name( + git_buf *out, + git_repository *repo, + const char *refname) +{ + GIT_BUF_WRAP_PRIVATE(out, git_branch__remote_name, repo, refname); +} + +int git_branch__remote_name( + git_str *out, + git_repository *repo, + const char *refname) +{ + git_strarray remote_list = {0}; + size_t i; + git_remote *remote; + const git_refspec *fetchspec; + int error = 0; + char *remote_name = NULL; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(refname); + + /* Verify that this is a remote branch */ + if (!git_reference__is_remote(refname)) { + git_error_set(GIT_ERROR_INVALID, "reference '%s' is not a remote branch.", + refname); + error = GIT_ERROR; + goto cleanup; + } + + /* Get the remotes */ + if ((error = git_remote_list(&remote_list, repo)) < 0) + goto cleanup; + + /* Find matching remotes */ + for (i = 0; i < remote_list.count; i++) { + if ((error = git_remote_lookup(&remote, repo, remote_list.strings[i])) < 0) + continue; + + fetchspec = git_remote__matching_dst_refspec(remote, refname); + if (fetchspec) { + /* If we have not already set out yet, then set + * it to the matching remote name. Otherwise + * multiple remotes match this reference, and it + * is ambiguous. */ + if (!remote_name) { + remote_name = remote_list.strings[i]; + } else { + git_remote_free(remote); + + git_error_set(GIT_ERROR_REFERENCE, + "reference '%s' is ambiguous", refname); + error = GIT_EAMBIGUOUS; + goto cleanup; + } + } + + git_remote_free(remote); + } + + if (remote_name) { + git_str_clear(out); + error = git_str_puts(out, remote_name); + } else { + git_error_set(GIT_ERROR_REFERENCE, + "could not determine remote for '%s'", refname); + error = GIT_ENOTFOUND; + } + +cleanup: + if (error < 0) + git_str_dispose(out); + + git_strarray_dispose(&remote_list); + return error; +} + +int git_branch_upstream( + git_reference **tracking_out, + const git_reference *branch) +{ + int error; + git_str tracking_name = GIT_STR_INIT; + + if ((error = git_branch__upstream_name(&tracking_name, + git_reference_owner(branch), git_reference_name(branch))) < 0) + return error; + + error = git_reference_lookup( + tracking_out, + git_reference_owner(branch), + git_str_cstr(&tracking_name)); + + git_str_dispose(&tracking_name); + return error; +} + +static int unset_upstream(git_config *config, const char *shortname) +{ + git_str buf = GIT_STR_INIT; + + if (git_str_printf(&buf, "branch.%s.remote", shortname) < 0) + return -1; + + if (git_config_delete_entry(config, git_str_cstr(&buf)) < 0) + goto on_error; + + git_str_clear(&buf); + if (git_str_printf(&buf, "branch.%s.merge", shortname) < 0) + goto on_error; + + if (git_config_delete_entry(config, git_str_cstr(&buf)) < 0) + goto on_error; + + git_str_dispose(&buf); + return 0; + +on_error: + git_str_dispose(&buf); + return -1; +} + +int git_branch_set_upstream(git_reference *branch, const char *branch_name) +{ + git_str key = GIT_STR_INIT, remote_name = GIT_STR_INIT, merge_refspec = GIT_STR_INIT; + git_reference *upstream; + git_repository *repo; + git_remote *remote = NULL; + git_config *config; + const char *refname, *shortname; + int local, error; + const git_refspec *fetchspec; + + refname = git_reference_name(branch); + if (!git_reference__is_branch(refname)) + return not_a_local_branch(refname); + + if (git_repository_config__weakptr(&config, git_reference_owner(branch)) < 0) + return -1; + + shortname = refname + strlen(GIT_REFS_HEADS_DIR); + + /* We're unsetting, delegate and bail-out */ + if (branch_name == NULL) + return unset_upstream(config, shortname); + + repo = git_reference_owner(branch); + + /* First we need to resolve name to a branch */ + if (git_branch_lookup(&upstream, repo, branch_name, GIT_BRANCH_LOCAL) == 0) + local = 1; + else if (git_branch_lookup(&upstream, repo, branch_name, GIT_BRANCH_REMOTE) == 0) + local = 0; + else { + git_error_set(GIT_ERROR_REFERENCE, + "cannot set upstream for branch '%s'", shortname); + return GIT_ENOTFOUND; + } + + /* + * If it's a local-tracking branch, its remote is "." (as "the local + * repository"), and the branch name is simply the refname. + * Otherwise we need to figure out what the remote-tracking branch's + * name on the remote is and use that. + */ + if (local) + error = git_str_puts(&remote_name, "."); + else + error = git_branch__remote_name(&remote_name, repo, git_reference_name(upstream)); + + if (error < 0) + goto on_error; + + /* Update the upstream branch config with the new name */ + if (git_str_printf(&key, "branch.%s.remote", shortname) < 0) + goto on_error; + + if (git_config_set_string(config, git_str_cstr(&key), git_str_cstr(&remote_name)) < 0) + goto on_error; + + if (local) { + /* A local branch uses the upstream refname directly */ + if (git_str_puts(&merge_refspec, git_reference_name(upstream)) < 0) + goto on_error; + } else { + /* We transform the upstream branch name according to the remote's refspecs */ + if (git_remote_lookup(&remote, repo, git_str_cstr(&remote_name)) < 0) + goto on_error; + + fetchspec = git_remote__matching_dst_refspec(remote, git_reference_name(upstream)); + if (!fetchspec || git_refspec__rtransform(&merge_refspec, fetchspec, git_reference_name(upstream)) < 0) + goto on_error; + + git_remote_free(remote); + remote = NULL; + } + + /* Update the merge branch config with the refspec */ + git_str_clear(&key); + if (git_str_printf(&key, "branch.%s.merge", shortname) < 0) + goto on_error; + + if (git_config_set_string(config, git_str_cstr(&key), git_str_cstr(&merge_refspec)) < 0) + goto on_error; + + git_reference_free(upstream); + git_str_dispose(&key); + git_str_dispose(&remote_name); + git_str_dispose(&merge_refspec); + + return 0; + +on_error: + git_reference_free(upstream); + git_str_dispose(&key); + git_str_dispose(&remote_name); + git_str_dispose(&merge_refspec); + git_remote_free(remote); + + return -1; +} + +int git_branch_is_head( + const git_reference *branch) +{ + git_reference *head; + bool is_same = false; + int error; + + GIT_ASSERT_ARG(branch); + + if (!git_reference_is_branch(branch)) + return false; + + error = git_repository_head(&head, git_reference_owner(branch)); + + if (error == GIT_EUNBORNBRANCH || error == GIT_ENOTFOUND) + return false; + + if (error < 0) + return -1; + + is_same = strcmp( + git_reference_name(branch), + git_reference_name(head)) == 0; + + git_reference_free(head); + + return is_same; +} + +int git_branch_name_is_valid(int *valid, const char *name) +{ + git_str ref_name = GIT_STR_INIT; + int error = 0; + + GIT_ASSERT(valid); + + *valid = 0; + + if (!name || !branch_name_is_valid(name)) + goto done; + + if ((error = git_str_puts(&ref_name, GIT_REFS_HEADS_DIR)) < 0 || + (error = git_str_puts(&ref_name, name)) < 0) + goto done; + + error = git_reference_name_is_valid(valid, ref_name.ptr); + +done: + git_str_dispose(&ref_name); + return error; +} diff --git a/src/libgit2/branch.h b/src/libgit2/branch.h new file mode 100644 index 0000000..b4db42a --- /dev/null +++ b/src/libgit2/branch.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_branch_h__ +#define INCLUDE_branch_h__ + +#include "common.h" + +#include "str.h" + +int git_branch__remote_name( + git_str *out, + git_repository *repo, + const char *refname); +int git_branch__upstream_remote( + git_str *out, + git_repository *repo, + const char *refname); +int git_branch__upstream_merge( + git_str *out, + git_repository *repo, + const char *refname); +int git_branch__upstream_name( + git_str *tracking_name, + git_repository *repo, + const char *canonical_branch_name); + +#endif diff --git a/src/libgit2/buf.c b/src/libgit2/buf.c new file mode 100644 index 0000000..652f5dd --- /dev/null +++ b/src/libgit2/buf.c @@ -0,0 +1,126 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "buf.h" +#include "common.h" + +int git_buf_sanitize(git_buf *buf) +{ + GIT_ASSERT_ARG(buf); + + if (buf->reserved > 0) + buf->ptr[0] = '\0'; + else + buf->ptr = git_str__initstr; + + buf->size = 0; + return 0; +} + +int git_buf_tostr(git_str *out, git_buf *buf) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(buf); + + if (git_buf_sanitize(buf) < 0) + return -1; + + out->ptr = buf->ptr; + out->asize = buf->reserved; + out->size = buf->size; + + buf->ptr = git_str__initstr; + buf->reserved = 0; + buf->size = 0; + + return 0; +} + +int git_buf_fromstr(git_buf *out, git_str *str) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(str); + + out->ptr = str->ptr; + out->reserved = str->asize; + out->size = str->size; + + str->ptr = git_str__initstr; + str->asize = 0; + str->size = 0; + + return 0; +} + +void git_buf_dispose(git_buf *buf) +{ + if (!buf) + return; + + if (buf->ptr != git_str__initstr) + git__free(buf->ptr); + + buf->ptr = git_str__initstr; + buf->reserved = 0; + buf->size = 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_buf_grow(git_buf *buffer, size_t target_size) +{ + char *newptr; + + if (buffer->reserved >= target_size) + return 0; + + if (buffer->ptr == git_str__initstr) + newptr = git__malloc(target_size); + else + newptr = git__realloc(buffer->ptr, target_size); + + if (!newptr) + return -1; + + buffer->ptr = newptr; + buffer->reserved = target_size; + return 0; +} + +int git_buf_set(git_buf *buffer, const void *data, size_t datalen) +{ + size_t alloclen; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, datalen, 1); + + if (git_buf_grow(buffer, alloclen) < 0) + return -1; + + memmove(buffer->ptr, data, datalen); + buffer->size = datalen; + buffer->ptr[buffer->size] = '\0'; + + return 0; +} + +int git_buf_is_binary(const git_buf *buf) +{ + git_str str = GIT_STR_INIT_CONST(buf->ptr, buf->size); + return git_str_is_binary(&str); +} + +int git_buf_contains_nul(const git_buf *buf) +{ + git_str str = GIT_STR_INIT_CONST(buf->ptr, buf->size); + return git_str_contains_nul(&str); +} + +void git_buf_free(git_buf *buffer) +{ + git_buf_dispose(buffer); +} + +#endif diff --git a/src/libgit2/buf.h b/src/libgit2/buf.h new file mode 100644 index 0000000..4bc7f27 --- /dev/null +++ b/src/libgit2/buf.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_buf_h__ +#define INCLUDE_buf_h__ + +#include "git2/buffer.h" +#include "common.h" + +/* + * Adapts a private API that takes a `git_str` into a public API that + * takes a `git_buf`. + */ + +#define GIT_BUF_WRAP_PRIVATE(buf, fn, ...) \ + { \ + git_str str = GIT_STR_INIT; \ + int error; \ + if ((error = git_buf_tostr(&str, buf)) == 0 && \ + (error = fn(&str, __VA_ARGS__)) == 0) \ + error = git_buf_fromstr(buf, &str); \ + git_str_dispose(&str); \ + return error; \ +} + +/** + * "Sanitizes" a buffer from user input. This simply ensures that the + * `git_buf` has nice defaults if the user didn't set the members to + * anything, so that if we return early we don't leave it populated + * with nonsense. + */ +extern int git_buf_sanitize(git_buf *from_user); + +/** + * Populate a `git_str` from a `git_buf` for passing to libgit2 internal + * functions. Sanitizes the given `git_buf` before proceeding. The + * `git_buf` will no longer point to this memory. + */ +extern int git_buf_tostr(git_str *out, git_buf *buf); + +/** + * Populate a `git_buf` from a `git_str` for returning to a user. + * The `git_str` will no longer point to this memory. + */ +extern int git_buf_fromstr(git_buf *out, git_str *str); + +#endif diff --git a/src/libgit2/cache.c b/src/libgit2/cache.c new file mode 100644 index 0000000..2f68e35 --- /dev/null +++ b/src/libgit2/cache.c @@ -0,0 +1,253 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "cache.h" + +#include "repository.h" +#include "commit.h" +#include "thread.h" +#include "util.h" +#include "odb.h" +#include "object.h" +#include "git2/oid.h" + +bool git_cache__enabled = true; +ssize_t git_cache__max_storage = (256 * 1024 * 1024); +git_atomic_ssize git_cache__current_storage = {0}; + +static size_t git_cache__max_object_size[8] = { + 0, /* GIT_OBJECT__EXT1 */ + 4096, /* GIT_OBJECT_COMMIT */ + 4096, /* GIT_OBJECT_TREE */ + 0, /* GIT_OBJECT_BLOB */ + 4096, /* GIT_OBJECT_TAG */ + 0, /* GIT_OBJECT__EXT2 */ + 0, /* GIT_OBJECT_OFS_DELTA */ + 0 /* GIT_OBJECT_REF_DELTA */ +}; + +int git_cache_set_max_object_size(git_object_t type, size_t size) +{ + if (type < 0 || (size_t)type >= ARRAY_SIZE(git_cache__max_object_size)) { + git_error_set(GIT_ERROR_INVALID, "type out of range"); + return -1; + } + + git_cache__max_object_size[type] = size; + return 0; +} + +int git_cache_init(git_cache *cache) +{ + memset(cache, 0, sizeof(*cache)); + + if ((git_oidmap_new(&cache->map)) < 0) + return -1; + + if (git_rwlock_init(&cache->lock)) { + git_error_set(GIT_ERROR_OS, "failed to initialize cache rwlock"); + return -1; + } + + return 0; +} + +/* called with lock */ +static void clear_cache(git_cache *cache) +{ + git_cached_obj *evict = NULL; + + if (git_cache_size(cache) == 0) + return; + + git_oidmap_foreach_value(cache->map, evict, { + git_cached_obj_decref(evict); + }); + + git_oidmap_clear(cache->map); + git_atomic_ssize_add(&git_cache__current_storage, -cache->used_memory); + cache->used_memory = 0; +} + +void git_cache_clear(git_cache *cache) +{ + if (git_rwlock_wrlock(&cache->lock) < 0) + return; + + clear_cache(cache); + + git_rwlock_wrunlock(&cache->lock); +} + +void git_cache_dispose(git_cache *cache) +{ + git_cache_clear(cache); + git_oidmap_free(cache->map); + git_rwlock_free(&cache->lock); + git__memzero(cache, sizeof(*cache)); +} + +/* Called with lock */ +static void cache_evict_entries(git_cache *cache) +{ + size_t evict_count = git_cache_size(cache) / 2048, i; + ssize_t evicted_memory = 0; + + if (evict_count < 8) + evict_count = 8; + + /* do not infinite loop if there's not enough entries to evict */ + if (evict_count > git_cache_size(cache)) { + clear_cache(cache); + return; + } + + i = 0; + while (evict_count > 0) { + git_cached_obj *evict; + const git_oid *key; + + if (git_oidmap_iterate((void **) &evict, cache->map, &i, &key) == GIT_ITEROVER) + break; + + evict_count--; + evicted_memory += evict->size; + git_oidmap_delete(cache->map, key); + git_cached_obj_decref(evict); + } + + cache->used_memory -= evicted_memory; + git_atomic_ssize_add(&git_cache__current_storage, -evicted_memory); +} + +static bool cache_should_store(git_object_t object_type, size_t object_size) +{ + size_t max_size = git_cache__max_object_size[object_type]; + return git_cache__enabled && object_size < max_size; +} + +static void *cache_get(git_cache *cache, const git_oid *oid, unsigned int flags) +{ + git_cached_obj *entry; + + if (!git_cache__enabled || git_rwlock_rdlock(&cache->lock) < 0) + return NULL; + + if ((entry = git_oidmap_get(cache->map, oid)) != NULL) { + if (flags && entry->flags != flags) { + entry = NULL; + } else { + git_cached_obj_incref(entry); + } + } + + git_rwlock_rdunlock(&cache->lock); + + return entry; +} + +static void *cache_store(git_cache *cache, git_cached_obj *entry) +{ + git_cached_obj *stored_entry; + + git_cached_obj_incref(entry); + + if (!git_cache__enabled && cache->used_memory > 0) { + git_cache_clear(cache); + return entry; + } + + if (!cache_should_store(entry->type, entry->size)) + return entry; + + if (git_rwlock_wrlock(&cache->lock) < 0) + return entry; + + /* soften the load on the cache */ + if (git_atomic_ssize_get(&git_cache__current_storage) > git_cache__max_storage) + cache_evict_entries(cache); + + /* not found */ + if ((stored_entry = git_oidmap_get(cache->map, &entry->oid)) == NULL) { + if (git_oidmap_set(cache->map, &entry->oid, entry) == 0) { + git_cached_obj_incref(entry); + cache->used_memory += entry->size; + git_atomic_ssize_add(&git_cache__current_storage, (ssize_t)entry->size); + } + } + /* found */ + else { + if (stored_entry->flags == entry->flags) { + git_cached_obj_decref(entry); + git_cached_obj_incref(stored_entry); + entry = stored_entry; + } else if (stored_entry->flags == GIT_CACHE_STORE_RAW && + entry->flags == GIT_CACHE_STORE_PARSED) { + if (git_oidmap_set(cache->map, &entry->oid, entry) == 0) { + git_cached_obj_decref(stored_entry); + git_cached_obj_incref(entry); + } else { + git_cached_obj_decref(entry); + git_cached_obj_incref(stored_entry); + entry = stored_entry; + } + } else { + /* NO OP */ + } + } + + git_rwlock_wrunlock(&cache->lock); + return entry; +} + +void *git_cache_store_raw(git_cache *cache, git_odb_object *entry) +{ + entry->cached.flags = GIT_CACHE_STORE_RAW; + return cache_store(cache, (git_cached_obj *)entry); +} + +void *git_cache_store_parsed(git_cache *cache, git_object *entry) +{ + entry->cached.flags = GIT_CACHE_STORE_PARSED; + return cache_store(cache, (git_cached_obj *)entry); +} + +git_odb_object *git_cache_get_raw(git_cache *cache, const git_oid *oid) +{ + return cache_get(cache, oid, GIT_CACHE_STORE_RAW); +} + +git_object *git_cache_get_parsed(git_cache *cache, const git_oid *oid) +{ + return cache_get(cache, oid, GIT_CACHE_STORE_PARSED); +} + +void *git_cache_get_any(git_cache *cache, const git_oid *oid) +{ + return cache_get(cache, oid, GIT_CACHE_STORE_ANY); +} + +void git_cached_obj_decref(void *_obj) +{ + git_cached_obj *obj = _obj; + + if (git_atomic32_dec(&obj->refcount) == 0) { + switch (obj->flags) { + case GIT_CACHE_STORE_RAW: + git_odb_object__free(_obj); + break; + + case GIT_CACHE_STORE_PARSED: + git_object__free(_obj); + break; + + default: + git__free(_obj); + break; + } + } +} diff --git a/src/libgit2/cache.h b/src/libgit2/cache.h new file mode 100644 index 0000000..42c4fa8 --- /dev/null +++ b/src/libgit2/cache.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_cache_h__ +#define INCLUDE_cache_h__ + +#include "common.h" + +#include "git2/common.h" +#include "git2/oid.h" +#include "git2/odb.h" + +#include "thread.h" +#include "oidmap.h" + +enum { + GIT_CACHE_STORE_ANY = 0, + GIT_CACHE_STORE_RAW = 1, + GIT_CACHE_STORE_PARSED = 2 +}; + +typedef struct { + git_oid oid; + int16_t type; /* git_object_t value */ + uint16_t flags; /* GIT_CACHE_STORE value */ + size_t size; + git_atomic32 refcount; +} git_cached_obj; + +typedef struct { + git_oidmap *map; + git_rwlock lock; + ssize_t used_memory; +} git_cache; + +extern bool git_cache__enabled; +extern ssize_t git_cache__max_storage; +extern git_atomic_ssize git_cache__current_storage; + +int git_cache_set_max_object_size(git_object_t type, size_t size); + +int git_cache_init(git_cache *cache); +void git_cache_dispose(git_cache *cache); +void git_cache_clear(git_cache *cache); + +void *git_cache_store_raw(git_cache *cache, git_odb_object *entry); +void *git_cache_store_parsed(git_cache *cache, git_object *entry); + +git_odb_object *git_cache_get_raw(git_cache *cache, const git_oid *oid); +git_object *git_cache_get_parsed(git_cache *cache, const git_oid *oid); +void *git_cache_get_any(git_cache *cache, const git_oid *oid); + +GIT_INLINE(size_t) git_cache_size(git_cache *cache) +{ + return (size_t)git_oidmap_size(cache->map); +} + +GIT_INLINE(void) git_cached_obj_incref(void *_obj) +{ + git_cached_obj *obj = _obj; + git_atomic32_inc(&obj->refcount); +} + +void git_cached_obj_decref(void *_obj); + +#endif diff --git a/src/libgit2/checkout.c b/src/libgit2/checkout.c new file mode 100644 index 0000000..6a46431 --- /dev/null +++ b/src/libgit2/checkout.c @@ -0,0 +1,2813 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "checkout.h" + +#include "git2/repository.h" +#include "git2/refs.h" +#include "git2/tree.h" +#include "git2/blob.h" +#include "git2/config.h" +#include "git2/diff.h" +#include "git2/submodule.h" +#include "git2/sys/index.h" +#include "git2/sys/filter.h" +#include "git2/merge.h" + +#include "refs.h" +#include "repository.h" +#include "index.h" +#include "filter.h" +#include "blob.h" +#include "diff.h" +#include "diff_generate.h" +#include "pathspec.h" +#include "diff_xdiff.h" +#include "fs_path.h" +#include "attr.h" +#include "pool.h" +#include "strmap.h" +#include "path.h" + +/* See docs/checkout-internals.md for more information */ + +enum { + CHECKOUT_ACTION__NONE = 0, + CHECKOUT_ACTION__REMOVE = 1, + CHECKOUT_ACTION__UPDATE_BLOB = 2, + CHECKOUT_ACTION__UPDATE_SUBMODULE = 4, + CHECKOUT_ACTION__CONFLICT = 8, + CHECKOUT_ACTION__REMOVE_CONFLICT = 16, + CHECKOUT_ACTION__UPDATE_CONFLICT = 32, + CHECKOUT_ACTION__MAX = 32, + CHECKOUT_ACTION__REMOVE_AND_UPDATE = + (CHECKOUT_ACTION__UPDATE_BLOB | CHECKOUT_ACTION__REMOVE) +}; + +typedef struct { + git_repository *repo; + git_iterator *target; + git_diff *diff; + git_checkout_options opts; + bool opts_free_baseline; + char *pfx; + git_index *index; + git_pool pool; + git_vector removes; + git_vector remove_conflicts; + git_vector update_conflicts; + git_vector *update_reuc; + git_vector *update_names; + git_str target_path; + size_t target_len; + git_str tmp; + unsigned int strategy; + int can_symlink; + int respect_filemode; + bool reload_submodules; + size_t total_steps; + size_t completed_steps; + git_checkout_perfdata perfdata; + git_strmap *mkdir_map; + git_attr_session attr_session; +} checkout_data; + +typedef struct { + const git_index_entry *ancestor; + const git_index_entry *ours; + const git_index_entry *theirs; + + unsigned int name_collision:1, + directoryfile:1, + one_to_two:1, + binary:1, + submodule:1; +} checkout_conflictdata; + +static int checkout_notify( + checkout_data *data, + git_checkout_notify_t why, + const git_diff_delta *delta, + const git_index_entry *wditem) +{ + git_diff_file wdfile; + const git_diff_file *baseline = NULL, *target = NULL, *workdir = NULL; + const char *path = NULL; + + if (!data->opts.notify_cb || + (why & data->opts.notify_flags) == 0) + return 0; + + if (wditem) { + memset(&wdfile, 0, sizeof(wdfile)); + + git_oid_cpy(&wdfile.id, &wditem->id); + wdfile.path = wditem->path; + wdfile.size = wditem->file_size; + wdfile.flags = GIT_DIFF_FLAG_VALID_ID; + wdfile.mode = wditem->mode; + + workdir = &wdfile; + + path = wditem->path; + } + + if (delta) { + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + case GIT_DELTA_MODIFIED: + case GIT_DELTA_TYPECHANGE: + default: + baseline = &delta->old_file; + target = &delta->new_file; + break; + case GIT_DELTA_ADDED: + case GIT_DELTA_IGNORED: + case GIT_DELTA_UNTRACKED: + case GIT_DELTA_UNREADABLE: + target = &delta->new_file; + break; + case GIT_DELTA_DELETED: + baseline = &delta->old_file; + break; + } + + path = delta->old_file.path; + } + + { + int error = data->opts.notify_cb( + why, path, baseline, target, workdir, data->opts.notify_payload); + + return git_error_set_after_callback_function( + error, "git_checkout notification"); + } +} + +GIT_INLINE(bool) is_workdir_base_or_new( + const git_oid *workdir_id, + const git_diff_file *baseitem, + const git_diff_file *newitem) +{ + return (git_oid__cmp(&baseitem->id, workdir_id) == 0 || + git_oid__cmp(&newitem->id, workdir_id) == 0); +} + +GIT_INLINE(bool) is_filemode_changed(git_filemode_t a, git_filemode_t b, int respect_filemode) +{ + /* If core.filemode = false, ignore links in the repository and executable bit changes */ + if (!respect_filemode) { + if (a == S_IFLNK) + a = GIT_FILEMODE_BLOB; + if (b == S_IFLNK) + b = GIT_FILEMODE_BLOB; + + a &= ~0111; + b &= ~0111; + } + + return (a != b); +} + +static bool checkout_is_workdir_modified( + checkout_data *data, + const git_diff_file *baseitem, + const git_diff_file *newitem, + const git_index_entry *wditem) +{ + git_oid oid; + const git_index_entry *ie; + + /* handle "modified" submodule */ + if (wditem->mode == GIT_FILEMODE_COMMIT) { + git_submodule *sm; + unsigned int sm_status = 0; + const git_oid *sm_oid = NULL; + bool rval = false; + + if (git_submodule_lookup(&sm, data->repo, wditem->path) < 0) { + git_error_clear(); + return true; + } + + if (git_submodule_status(&sm_status, data->repo, wditem->path, GIT_SUBMODULE_IGNORE_UNSPECIFIED) < 0 || + GIT_SUBMODULE_STATUS_IS_WD_DIRTY(sm_status)) + rval = true; + else if ((sm_oid = git_submodule_wd_id(sm)) == NULL) + rval = false; + else + rval = (git_oid__cmp(&baseitem->id, sm_oid) != 0); + + git_submodule_free(sm); + return rval; + } + + /* + * Look at the cache to decide if the workdir is modified: if the + * cache contents match the workdir contents, then we do not need + * to examine the working directory directly, instead we can + * examine the cache to see if _it_ has been modified. This allows + * us to avoid touching the disk. + */ + ie = git_index_get_bypath(data->index, wditem->path, 0); + + if (ie != NULL && + !git_index_entry_newer_than_index(ie, data->index) && + git_index_time_eq(&wditem->mtime, &ie->mtime) && + wditem->file_size == ie->file_size && + !is_filemode_changed(wditem->mode, ie->mode, data->respect_filemode)) { + + /* The workdir is modified iff the index entry is modified */ + return !is_workdir_base_or_new(&ie->id, baseitem, newitem) || + is_filemode_changed(baseitem->mode, ie->mode, data->respect_filemode); + } + + /* depending on where base is coming from, we may or may not know + * the actual size of the data, so we can't rely on this shortcut. + */ + if (baseitem->size && wditem->file_size != baseitem->size) + return true; + + /* if the workdir item is a directory, it cannot be a modified file */ + if (S_ISDIR(wditem->mode)) + return false; + + if (is_filemode_changed(baseitem->mode, wditem->mode, data->respect_filemode)) + return true; + + if (git_diff__oid_for_entry(&oid, data->diff, wditem, wditem->mode, NULL) < 0) + return false; + + /* Allow the checkout if the workdir is not modified *or* if the checkout + * target's contents are already in the working directory. + */ + return !is_workdir_base_or_new(&oid, baseitem, newitem); +} + +#define CHECKOUT_ACTION_IF(FLAG,YES,NO) \ + ((data->strategy & GIT_CHECKOUT_##FLAG) ? CHECKOUT_ACTION__##YES : CHECKOUT_ACTION__##NO) + +static int checkout_action_common( + int *action, + checkout_data *data, + const git_diff_delta *delta, + const git_index_entry *wd) +{ + git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE; + + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) + *action = (*action & ~CHECKOUT_ACTION__REMOVE); + + if ((*action & CHECKOUT_ACTION__UPDATE_BLOB) != 0) { + if (S_ISGITLINK(delta->new_file.mode)) + *action = (*action & ~CHECKOUT_ACTION__UPDATE_BLOB) | + CHECKOUT_ACTION__UPDATE_SUBMODULE; + + /* to "update" a symlink, we must remove the old one first */ + if (delta->new_file.mode == GIT_FILEMODE_LINK && wd != NULL) + *action |= CHECKOUT_ACTION__REMOVE; + + /* if the file is on disk and doesn't match our mode, force update */ + if (wd && + GIT_PERMS_IS_EXEC(wd->mode) != GIT_PERMS_IS_EXEC(delta->new_file.mode)) + *action |= CHECKOUT_ACTION__REMOVE; + + notify = GIT_CHECKOUT_NOTIFY_UPDATED; + } + + if ((*action & CHECKOUT_ACTION__CONFLICT) != 0) + notify = GIT_CHECKOUT_NOTIFY_CONFLICT; + + return checkout_notify(data, notify, delta, wd); +} + +static int checkout_action_no_wd( + int *action, + checkout_data *data, + const git_diff_delta *delta) +{ + int error = 0; + + *action = CHECKOUT_ACTION__NONE; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: /* case 12 */ + error = checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, NULL); + if (error) + return error; + *action = CHECKOUT_ACTION_IF(RECREATE_MISSING, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_ADDED: /* case 2 or 28 (and 5 but not really) */ + *action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_MODIFIED: /* case 13 (and 35 but not really) */ + *action = CHECKOUT_ACTION_IF(RECREATE_MISSING, UPDATE_BLOB, CONFLICT); + break; + case GIT_DELTA_TYPECHANGE: /* case 21 (B->T) and 28 (T->B)*/ + if (delta->new_file.mode == GIT_FILEMODE_TREE) + *action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_DELETED: /* case 8 or 25 */ + *action = CHECKOUT_ACTION_IF(SAFE, REMOVE, NONE); + break; + default: /* impossible */ + break; + } + + return checkout_action_common(action, data, delta, NULL); +} + +static int checkout_target_fullpath( + git_str **out, checkout_data *data, const char *path) +{ + git_str_truncate(&data->target_path, data->target_len); + + if (path && git_str_puts(&data->target_path, path) < 0) + return -1; + + if (git_path_validate_str_length(data->repo, &data->target_path) < 0) + return -1; + + *out = &data->target_path; + + return 0; +} + +static bool wd_item_is_removable( + checkout_data *data, const git_index_entry *wd) +{ + git_str *full; + + if (wd->mode != GIT_FILEMODE_TREE) + return true; + + if (checkout_target_fullpath(&full, data, wd->path) < 0) + return false; + + return !full || !git_fs_path_contains(full, DOT_GIT); +} + +static int checkout_queue_remove(checkout_data *data, const char *path) +{ + char *copy = git_pool_strdup(&data->pool, path); + GIT_ERROR_CHECK_ALLOC(copy); + return git_vector_insert(&data->removes, copy); +} + +/* note that this advances the iterator over the wd item */ +static int checkout_action_wd_only( + checkout_data *data, + git_iterator *workdir, + const git_index_entry **wditem, + git_vector *pathspec) +{ + int error = 0; + bool remove = false; + git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE; + const git_index_entry *wd = *wditem; + + if (!git_pathspec__match( + pathspec, wd->path, + (data->strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) != 0, + git_iterator_ignore_case(workdir), NULL, NULL)) { + + if (wd->mode == GIT_FILEMODE_TREE) + return git_iterator_advance_into(wditem, workdir); + else + return git_iterator_advance(wditem, workdir); + } + + /* check if item is tracked in the index but not in the checkout diff */ + if (data->index != NULL) { + size_t pos; + + error = git_index__find_pos( + &pos, data->index, wd->path, 0, GIT_INDEX_STAGE_ANY); + + if (wd->mode != GIT_FILEMODE_TREE) { + if (!error) { /* found by git_index__find_pos call */ + notify = GIT_CHECKOUT_NOTIFY_DIRTY; + remove = ((data->strategy & GIT_CHECKOUT_FORCE) != 0); + } else if (error != GIT_ENOTFOUND) + return error; + else + error = 0; /* git_index__find_pos does not set error msg */ + } else { + /* for tree entries, we have to see if there are any index + * entries that are contained inside that tree + */ + const git_index_entry *e = git_index_get_byindex(data->index, pos); + + if (e != NULL && data->diff->pfxcomp(e->path, wd->path) == 0) + return git_iterator_advance_into(wditem, workdir); + } + } + + if (notify != GIT_CHECKOUT_NOTIFY_NONE) { + /* if we found something in the index, notify and advance */ + if ((error = checkout_notify(data, notify, NULL, wd)) != 0) + return error; + + if (remove && wd_item_is_removable(data, wd)) + error = checkout_queue_remove(data, wd->path); + + if (!error) + error = git_iterator_advance(wditem, workdir); + } else { + /* untracked or ignored - can't know which until we advance through */ + bool over = false, removable = wd_item_is_removable(data, wd); + git_iterator_status_t untracked_state; + + /* copy the entry for issuing notification callback later */ + git_index_entry saved_wd = *wd; + git_str_sets(&data->tmp, wd->path); + saved_wd.path = data->tmp.ptr; + + error = git_iterator_advance_over( + wditem, &untracked_state, workdir); + if (error == GIT_ITEROVER) + over = true; + else if (error < 0) + return error; + + if (untracked_state == GIT_ITERATOR_STATUS_IGNORED) { + notify = GIT_CHECKOUT_NOTIFY_IGNORED; + remove = ((data->strategy & GIT_CHECKOUT_REMOVE_IGNORED) != 0); + } else { + notify = GIT_CHECKOUT_NOTIFY_UNTRACKED; + remove = ((data->strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0); + } + + if ((error = checkout_notify(data, notify, NULL, &saved_wd)) != 0) + return error; + + if (remove && removable) + error = checkout_queue_remove(data, saved_wd.path); + + if (!error && over) /* restore ITEROVER if needed */ + error = GIT_ITEROVER; + } + + return error; +} + +static bool submodule_is_config_only( + checkout_data *data, + const char *path) +{ + git_submodule *sm = NULL; + unsigned int sm_loc = 0; + bool rval = false; + + if (git_submodule_lookup(&sm, data->repo, path) < 0) + return true; + + if (git_submodule_location(&sm_loc, sm) < 0 || + sm_loc == GIT_SUBMODULE_STATUS_IN_CONFIG) + rval = true; + + git_submodule_free(sm); + + return rval; +} + +static bool checkout_is_empty_dir(checkout_data *data, const char *path) +{ + git_str *fullpath; + + if (checkout_target_fullpath(&fullpath, data, path) < 0) + return false; + + return git_fs_path_is_empty_dir(fullpath->ptr); +} + +static int checkout_action_with_wd( + int *action, + checkout_data *data, + const git_diff_delta *delta, + git_iterator *workdir, + const git_index_entry *wd) +{ + *action = CHECKOUT_ACTION__NONE; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: /* case 14/15 or 33 */ + if (checkout_is_workdir_modified(data, &delta->old_file, &delta->new_file, wd)) { + GIT_ERROR_CHECK_ERROR( + checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, wd) ); + *action = CHECKOUT_ACTION_IF(FORCE, UPDATE_BLOB, NONE); + } + break; + case GIT_DELTA_ADDED: /* case 3, 4 or 6 */ + if (git_iterator_current_is_ignored(workdir)) + *action = CHECKOUT_ACTION_IF(DONT_OVERWRITE_IGNORED, CONFLICT, UPDATE_BLOB); + else + *action = CHECKOUT_ACTION_IF(FORCE, UPDATE_BLOB, CONFLICT); + break; + case GIT_DELTA_DELETED: /* case 9 or 10 (or 26 but not really) */ + if (checkout_is_workdir_modified(data, &delta->old_file, &delta->new_file, wd)) + *action = CHECKOUT_ACTION_IF(FORCE, REMOVE, CONFLICT); + else + *action = CHECKOUT_ACTION_IF(SAFE, REMOVE, NONE); + break; + case GIT_DELTA_MODIFIED: /* case 16, 17, 18 (or 36 but not really) */ + if (wd->mode != GIT_FILEMODE_COMMIT && + checkout_is_workdir_modified(data, &delta->old_file, &delta->new_file, wd)) + *action = CHECKOUT_ACTION_IF(FORCE, UPDATE_BLOB, CONFLICT); + else + *action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_TYPECHANGE: /* case 22, 23, 29, 30 */ + if (delta->old_file.mode == GIT_FILEMODE_TREE) { + if (wd->mode == GIT_FILEMODE_TREE) + /* either deleting items in old tree will delete the wd dir, + * or we'll get a conflict when we attempt blob update... + */ + *action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + else if (wd->mode == GIT_FILEMODE_COMMIT) { + /* workdir is possibly a "phantom" submodule - treat as a + * tree if the only submodule info came from the config + */ + if (submodule_is_config_only(data, wd->path)) + *action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + else + *action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + } else + *action = CHECKOUT_ACTION_IF(FORCE, REMOVE, CONFLICT); + } + else if (checkout_is_workdir_modified(data, &delta->old_file, &delta->new_file, wd)) + *action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + else + *action = CHECKOUT_ACTION_IF(SAFE, REMOVE_AND_UPDATE, NONE); + + /* don't update if the typechange is to a tree */ + if (delta->new_file.mode == GIT_FILEMODE_TREE) + *action = (*action & ~CHECKOUT_ACTION__UPDATE_BLOB); + break; + default: /* impossible */ + break; + } + + return checkout_action_common(action, data, delta, wd); +} + +static int checkout_action_with_wd_blocker( + int *action, + checkout_data *data, + const git_diff_delta *delta, + const git_index_entry *wd) +{ + *action = CHECKOUT_ACTION__NONE; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + /* should show delta as dirty / deleted */ + GIT_ERROR_CHECK_ERROR( + checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, wd) ); + *action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, NONE); + break; + case GIT_DELTA_ADDED: + case GIT_DELTA_MODIFIED: + *action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + break; + case GIT_DELTA_DELETED: + *action = CHECKOUT_ACTION_IF(FORCE, REMOVE, CONFLICT); + break; + case GIT_DELTA_TYPECHANGE: + /* not 100% certain about this... */ + *action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + break; + default: /* impossible */ + break; + } + + return checkout_action_common(action, data, delta, wd); +} + +static int checkout_action_with_wd_dir( + int *action, + checkout_data *data, + const git_diff_delta *delta, + git_iterator *workdir, + const git_index_entry *wd) +{ + *action = CHECKOUT_ACTION__NONE; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: /* case 19 or 24 (or 34 but not really) */ + GIT_ERROR_CHECK_ERROR( + checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, NULL)); + GIT_ERROR_CHECK_ERROR( + checkout_notify(data, GIT_CHECKOUT_NOTIFY_UNTRACKED, NULL, wd)); + *action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, NONE); + break; + case GIT_DELTA_ADDED:/* case 4 (and 7 for dir) */ + case GIT_DELTA_MODIFIED: /* case 20 (or 37 but not really) */ + if (delta->old_file.mode == GIT_FILEMODE_COMMIT) + /* expected submodule (and maybe found one) */; + else if (delta->new_file.mode != GIT_FILEMODE_TREE) + *action = git_iterator_current_is_ignored(workdir) ? + CHECKOUT_ACTION_IF(DONT_OVERWRITE_IGNORED, CONFLICT, REMOVE_AND_UPDATE) : + CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + break; + case GIT_DELTA_DELETED: /* case 11 (and 27 for dir) */ + if (delta->old_file.mode != GIT_FILEMODE_TREE) + GIT_ERROR_CHECK_ERROR( + checkout_notify(data, GIT_CHECKOUT_NOTIFY_UNTRACKED, NULL, wd)); + break; + case GIT_DELTA_TYPECHANGE: /* case 24 or 31 */ + if (delta->old_file.mode == GIT_FILEMODE_TREE) { + /* For typechange from dir, remove dir and add blob, but it is + * not safe to remove dir if it contains modified files. + * However, safely removing child files will remove the parent + * directory if is it left empty, so we can defer removing the + * dir and it will succeed if no children are left. + */ + *action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + } + else if (delta->new_file.mode != GIT_FILEMODE_TREE) + /* For typechange to dir, dir is already created so no action */ + *action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + break; + default: /* impossible */ + break; + } + + return checkout_action_common(action, data, delta, wd); +} + +static int checkout_action_with_wd_dir_empty( + int *action, + checkout_data *data, + const git_diff_delta *delta) +{ + int error = checkout_action_no_wd(action, data, delta); + + /* We can always safely remove an empty directory. */ + if (error == 0 && *action != CHECKOUT_ACTION__NONE) + *action |= CHECKOUT_ACTION__REMOVE; + + return error; +} + +static int checkout_action( + int *action, + checkout_data *data, + git_diff_delta *delta, + git_iterator *workdir, + const git_index_entry **wditem, + git_vector *pathspec) +{ + int cmp = -1, error; + int (*strcomp)(const char *, const char *) = data->diff->strcomp; + int (*pfxcomp)(const char *str, const char *pfx) = data->diff->pfxcomp; + int (*advance)(const git_index_entry **, git_iterator *) = NULL; + + /* move workdir iterator to follow along with deltas */ + + while (1) { + const git_index_entry *wd = *wditem; + + if (!wd) + return checkout_action_no_wd(action, data, delta); + + cmp = strcomp(wd->path, delta->old_file.path); + + /* 1. wd before delta ("a/a" before "a/b") + * 2. wd prefixes delta & should expand ("a/" before "a/b") + * 3. wd prefixes delta & cannot expand ("a/b" before "a/b/c") + * 4. wd equals delta ("a/b" and "a/b") + * 5. wd after delta & delta prefixes wd ("a/b/c" after "a/b/" or "a/b") + * 6. wd after delta ("a/c" after "a/b") + */ + + if (cmp < 0) { + cmp = pfxcomp(delta->old_file.path, wd->path); + + if (cmp == 0) { + if (wd->mode == GIT_FILEMODE_TREE) { + /* case 2 - entry prefixed by workdir tree */ + error = git_iterator_advance_into(wditem, workdir); + if (error < 0 && error != GIT_ITEROVER) + goto done; + continue; + } + + /* case 3 maybe - wd contains non-dir where dir expected */ + if (delta->old_file.path[strlen(wd->path)] == '/') { + error = checkout_action_with_wd_blocker( + action, data, delta, wd); + advance = git_iterator_advance; + goto done; + } + } + + /* case 1 - handle wd item (if it matches pathspec) */ + error = checkout_action_wd_only(data, workdir, wditem, pathspec); + if (error && error != GIT_ITEROVER) + goto done; + continue; + } + + if (cmp == 0) { + /* case 4 */ + error = checkout_action_with_wd(action, data, delta, workdir, wd); + advance = git_iterator_advance; + goto done; + } + + cmp = pfxcomp(wd->path, delta->old_file.path); + + if (cmp == 0) { /* case 5 */ + if (wd->path[strlen(delta->old_file.path)] != '/') + return checkout_action_no_wd(action, data, delta); + + if (delta->status == GIT_DELTA_TYPECHANGE) { + if (delta->old_file.mode == GIT_FILEMODE_TREE) { + error = checkout_action_with_wd(action, data, delta, workdir, wd); + advance = git_iterator_advance_into; + goto done; + } + + if (delta->new_file.mode == GIT_FILEMODE_TREE || + delta->new_file.mode == GIT_FILEMODE_COMMIT || + delta->old_file.mode == GIT_FILEMODE_COMMIT) + { + error = checkout_action_with_wd(action, data, delta, workdir, wd); + advance = git_iterator_advance; + goto done; + } + } + + return checkout_is_empty_dir(data, wd->path) ? + checkout_action_with_wd_dir_empty(action, data, delta) : + checkout_action_with_wd_dir(action, data, delta, workdir, wd); + } + + /* case 6 - wd is after delta */ + return checkout_action_no_wd(action, data, delta); + } + +done: + if (!error && advance != NULL && + (error = advance(wditem, workdir)) < 0) { + *wditem = NULL; + if (error == GIT_ITEROVER) + error = 0; + } + + return error; +} + +static int checkout_remaining_wd_items( + checkout_data *data, + git_iterator *workdir, + const git_index_entry *wd, + git_vector *spec) +{ + int error = 0; + + while (wd && !error) + error = checkout_action_wd_only(data, workdir, &wd, spec); + + if (error == GIT_ITEROVER) + error = 0; + + return error; +} + +GIT_INLINE(int) checkout_idxentry_cmp( + const git_index_entry *a, + const git_index_entry *b) +{ + if (!a && !b) + return 0; + else if (!a && b) + return -1; + else if(a && !b) + return 1; + else + return strcmp(a->path, b->path); +} + +static int checkout_conflictdata_cmp(const void *a, const void *b) +{ + const checkout_conflictdata *ca = a; + const checkout_conflictdata *cb = b; + int diff; + + if ((diff = checkout_idxentry_cmp(ca->ancestor, cb->ancestor)) == 0 && + (diff = checkout_idxentry_cmp(ca->ours, cb->theirs)) == 0) + diff = checkout_idxentry_cmp(ca->theirs, cb->theirs); + + return diff; +} + +static int checkout_conflictdata_empty( + const git_vector *conflicts, size_t idx, void *payload) +{ + checkout_conflictdata *conflict; + + GIT_UNUSED(payload); + + if ((conflict = git_vector_get(conflicts, idx)) == NULL) + return -1; + + if (conflict->ancestor || conflict->ours || conflict->theirs) + return 0; + + git__free(conflict); + return 1; +} + +GIT_INLINE(bool) conflict_pathspec_match( + checkout_data *data, + git_iterator *workdir, + git_vector *pathspec, + const git_index_entry *ancestor, + const git_index_entry *ours, + const git_index_entry *theirs) +{ + /* if the pathspec matches ours *or* theirs, proceed */ + if (ours && git_pathspec__match(pathspec, ours->path, + (data->strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) != 0, + git_iterator_ignore_case(workdir), NULL, NULL)) + return true; + + if (theirs && git_pathspec__match(pathspec, theirs->path, + (data->strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) != 0, + git_iterator_ignore_case(workdir), NULL, NULL)) + return true; + + if (ancestor && git_pathspec__match(pathspec, ancestor->path, + (data->strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) != 0, + git_iterator_ignore_case(workdir), NULL, NULL)) + return true; + + return false; +} + +GIT_INLINE(int) checkout_conflict_detect_submodule(checkout_conflictdata *conflict) +{ + conflict->submodule = ((conflict->ancestor && S_ISGITLINK(conflict->ancestor->mode)) || + (conflict->ours && S_ISGITLINK(conflict->ours->mode)) || + (conflict->theirs && S_ISGITLINK(conflict->theirs->mode))); + return 0; +} + +GIT_INLINE(int) checkout_conflict_detect_binary(git_repository *repo, checkout_conflictdata *conflict) +{ + git_blob *ancestor_blob = NULL, *our_blob = NULL, *their_blob = NULL; + int error = 0; + + if (conflict->submodule) + return 0; + + if (conflict->ancestor) { + if ((error = git_blob_lookup(&ancestor_blob, repo, &conflict->ancestor->id)) < 0) + goto done; + + conflict->binary = git_blob_is_binary(ancestor_blob); + } + + if (!conflict->binary && conflict->ours) { + if ((error = git_blob_lookup(&our_blob, repo, &conflict->ours->id)) < 0) + goto done; + + conflict->binary = git_blob_is_binary(our_blob); + } + + if (!conflict->binary && conflict->theirs) { + if ((error = git_blob_lookup(&their_blob, repo, &conflict->theirs->id)) < 0) + goto done; + + conflict->binary = git_blob_is_binary(their_blob); + } + +done: + git_blob_free(ancestor_blob); + git_blob_free(our_blob); + git_blob_free(their_blob); + + return error; +} + +static int checkout_conflict_append_update( + const git_index_entry *ancestor, + const git_index_entry *ours, + const git_index_entry *theirs, + void *payload) +{ + checkout_data *data = payload; + checkout_conflictdata *conflict; + int error; + + conflict = git__calloc(1, sizeof(checkout_conflictdata)); + GIT_ERROR_CHECK_ALLOC(conflict); + + conflict->ancestor = ancestor; + conflict->ours = ours; + conflict->theirs = theirs; + + if ((error = checkout_conflict_detect_submodule(conflict)) < 0 || + (error = checkout_conflict_detect_binary(data->repo, conflict)) < 0) + { + git__free(conflict); + return error; + } + + if (git_vector_insert(&data->update_conflicts, conflict)) + return -1; + + return 0; +} + +static int checkout_conflicts_foreach( + checkout_data *data, + git_index *index, + git_iterator *workdir, + git_vector *pathspec, + int (*cb)(const git_index_entry *, const git_index_entry *, const git_index_entry *, void *), + void *payload) +{ + git_index_conflict_iterator *iterator = NULL; + const git_index_entry *ancestor, *ours, *theirs; + int error = 0; + + if ((error = git_index_conflict_iterator_new(&iterator, index)) < 0) + goto done; + + /* Collect the conflicts */ + while ((error = git_index_conflict_next(&ancestor, &ours, &theirs, iterator)) == 0) { + if (!conflict_pathspec_match(data, workdir, pathspec, ancestor, ours, theirs)) + continue; + + if ((error = cb(ancestor, ours, theirs, payload)) < 0) + goto done; + } + + if (error == GIT_ITEROVER) + error = 0; + +done: + git_index_conflict_iterator_free(iterator); + + return error; +} + +static int checkout_conflicts_load(checkout_data *data, git_iterator *workdir, git_vector *pathspec) +{ + git_index *index; + + /* Only write conflicts from sources that have them: indexes. */ + if ((index = git_iterator_index(data->target)) == NULL) + return 0; + + data->update_conflicts._cmp = checkout_conflictdata_cmp; + + if (checkout_conflicts_foreach(data, index, workdir, pathspec, checkout_conflict_append_update, data) < 0) + return -1; + + /* Collect the REUC and NAME entries */ + data->update_reuc = &index->reuc; + data->update_names = &index->names; + + return 0; +} + +GIT_INLINE(int) checkout_conflicts_cmp_entry( + const char *path, + const git_index_entry *entry) +{ + return strcmp((const char *)path, entry->path); +} + +static int checkout_conflicts_cmp_ancestor(const void *p, const void *c) +{ + const char *path = p; + const checkout_conflictdata *conflict = c; + + if (!conflict->ancestor) + return 1; + + return checkout_conflicts_cmp_entry(path, conflict->ancestor); +} + +static checkout_conflictdata *checkout_conflicts_search_ancestor( + checkout_data *data, + const char *path) +{ + size_t pos; + + if (git_vector_bsearch2(&pos, &data->update_conflicts, checkout_conflicts_cmp_ancestor, path) < 0) + return NULL; + + return git_vector_get(&data->update_conflicts, pos); +} + +static checkout_conflictdata *checkout_conflicts_search_branch( + checkout_data *data, + const char *path) +{ + checkout_conflictdata *conflict; + size_t i; + + git_vector_foreach(&data->update_conflicts, i, conflict) { + int cmp = -1; + + if (conflict->ancestor) + break; + + if (conflict->ours) + cmp = checkout_conflicts_cmp_entry(path, conflict->ours); + else if (conflict->theirs) + cmp = checkout_conflicts_cmp_entry(path, conflict->theirs); + + if (cmp == 0) + return conflict; + } + + return NULL; +} + +static int checkout_conflicts_load_byname_entry( + checkout_conflictdata **ancestor_out, + checkout_conflictdata **ours_out, + checkout_conflictdata **theirs_out, + checkout_data *data, + const git_index_name_entry *name_entry) +{ + checkout_conflictdata *ancestor, *ours = NULL, *theirs = NULL; + int error = 0; + + *ancestor_out = NULL; + *ours_out = NULL; + *theirs_out = NULL; + + if (!name_entry->ancestor) { + git_error_set(GIT_ERROR_INDEX, "a NAME entry exists without an ancestor"); + error = -1; + goto done; + } + + if (!name_entry->ours && !name_entry->theirs) { + git_error_set(GIT_ERROR_INDEX, "a NAME entry exists without an ours or theirs"); + error = -1; + goto done; + } + + if ((ancestor = checkout_conflicts_search_ancestor(data, + name_entry->ancestor)) == NULL) { + git_error_set(GIT_ERROR_INDEX, + "a NAME entry referenced ancestor entry '%s' which does not exist in the main index", + name_entry->ancestor); + error = -1; + goto done; + } + + if (name_entry->ours) { + if (strcmp(name_entry->ancestor, name_entry->ours) == 0) + ours = ancestor; + else if ((ours = checkout_conflicts_search_branch(data, name_entry->ours)) == NULL || + ours->ours == NULL) { + git_error_set(GIT_ERROR_INDEX, + "a NAME entry referenced our entry '%s' which does not exist in the main index", + name_entry->ours); + error = -1; + goto done; + } + } + + if (name_entry->theirs) { + if (strcmp(name_entry->ancestor, name_entry->theirs) == 0) + theirs = ancestor; + else if (name_entry->ours && strcmp(name_entry->ours, name_entry->theirs) == 0) + theirs = ours; + else if ((theirs = checkout_conflicts_search_branch(data, name_entry->theirs)) == NULL || + theirs->theirs == NULL) { + git_error_set(GIT_ERROR_INDEX, + "a NAME entry referenced their entry '%s' which does not exist in the main index", + name_entry->theirs); + error = -1; + goto done; + } + } + + *ancestor_out = ancestor; + *ours_out = ours; + *theirs_out = theirs; + +done: + return error; +} + +static int checkout_conflicts_coalesce_renames( + checkout_data *data) +{ + git_index *index; + const git_index_name_entry *name_entry; + checkout_conflictdata *ancestor_conflict, *our_conflict, *their_conflict; + size_t i, names; + int error = 0; + + if ((index = git_iterator_index(data->target)) == NULL) + return 0; + + /* Juggle entries based on renames */ + names = git_index_name_entrycount(index); + + for (i = 0; i < names; i++) { + name_entry = git_index_name_get_byindex(index, i); + + if ((error = checkout_conflicts_load_byname_entry( + &ancestor_conflict, &our_conflict, &their_conflict, + data, name_entry)) < 0) + goto done; + + if (our_conflict && our_conflict != ancestor_conflict) { + ancestor_conflict->ours = our_conflict->ours; + our_conflict->ours = NULL; + + if (our_conflict->theirs) + our_conflict->name_collision = 1; + + if (our_conflict->name_collision) + ancestor_conflict->name_collision = 1; + } + + if (their_conflict && their_conflict != ancestor_conflict) { + ancestor_conflict->theirs = their_conflict->theirs; + their_conflict->theirs = NULL; + + if (their_conflict->ours) + their_conflict->name_collision = 1; + + if (their_conflict->name_collision) + ancestor_conflict->name_collision = 1; + } + + if (our_conflict && our_conflict != ancestor_conflict && + their_conflict && their_conflict != ancestor_conflict) + ancestor_conflict->one_to_two = 1; + } + + git_vector_remove_matching( + &data->update_conflicts, checkout_conflictdata_empty, NULL); + +done: + return error; +} + +static int checkout_conflicts_mark_directoryfile( + checkout_data *data) +{ + git_index *index; + checkout_conflictdata *conflict; + const git_index_entry *entry; + size_t i, j, len; + const char *path; + int prefixed, error = 0; + + if ((index = git_iterator_index(data->target)) == NULL) + return 0; + + len = git_index_entrycount(index); + + /* Find d/f conflicts */ + git_vector_foreach(&data->update_conflicts, i, conflict) { + if ((conflict->ours && conflict->theirs) || + (!conflict->ours && !conflict->theirs)) + continue; + + path = conflict->ours ? + conflict->ours->path : conflict->theirs->path; + + if ((error = git_index_find(&j, index, path)) < 0) { + if (error == GIT_ENOTFOUND) + git_error_set(GIT_ERROR_INDEX, + "index inconsistency, could not find entry for expected conflict '%s'", path); + + goto done; + } + + for (; j < len; j++) { + if ((entry = git_index_get_byindex(index, j)) == NULL) { + git_error_set(GIT_ERROR_INDEX, + "index inconsistency, truncated index while loading expected conflict '%s'", path); + error = -1; + goto done; + } + + prefixed = git_fs_path_equal_or_prefixed(path, entry->path, NULL); + + if (prefixed == GIT_FS_PATH_EQUAL) + continue; + + if (prefixed == GIT_FS_PATH_PREFIX) + conflict->directoryfile = 1; + + break; + } + } + +done: + return error; +} + +static int checkout_get_update_conflicts( + checkout_data *data, + git_iterator *workdir, + git_vector *pathspec) +{ + int error = 0; + + if (data->strategy & GIT_CHECKOUT_SKIP_UNMERGED) + return 0; + + if ((error = checkout_conflicts_load(data, workdir, pathspec)) < 0 || + (error = checkout_conflicts_coalesce_renames(data)) < 0 || + (error = checkout_conflicts_mark_directoryfile(data)) < 0) + goto done; + +done: + return error; +} + +static int checkout_conflict_append_remove( + const git_index_entry *ancestor, + const git_index_entry *ours, + const git_index_entry *theirs, + void *payload) +{ + checkout_data *data = payload; + const char *name; + + GIT_ASSERT_ARG(ancestor || ours || theirs); + + if (ancestor) + name = git__strdup(ancestor->path); + else if (ours) + name = git__strdup(ours->path); + else if (theirs) + name = git__strdup(theirs->path); + else + abort(); + + GIT_ERROR_CHECK_ALLOC(name); + + return git_vector_insert(&data->remove_conflicts, (char *)name); +} + +static int checkout_get_remove_conflicts( + checkout_data *data, + git_iterator *workdir, + git_vector *pathspec) +{ + if ((data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) != 0) + return 0; + + return checkout_conflicts_foreach(data, data->index, workdir, pathspec, checkout_conflict_append_remove, data); +} + +static int checkout_verify_paths( + git_repository *repo, + int action, + git_diff_delta *delta) +{ + unsigned int flags = GIT_PATH_REJECT_WORKDIR_DEFAULTS; + + if (action & CHECKOUT_ACTION__REMOVE) { + if (!git_path_is_valid(repo, delta->old_file.path, delta->old_file.mode, flags)) { + git_error_set(GIT_ERROR_CHECKOUT, "cannot remove invalid path '%s'", delta->old_file.path); + return -1; + } + } + + if (action & ~CHECKOUT_ACTION__REMOVE) { + if (!git_path_is_valid(repo, delta->new_file.path, delta->new_file.mode, flags)) { + git_error_set(GIT_ERROR_CHECKOUT, "cannot checkout to invalid path '%s'", delta->new_file.path); + return -1; + } + } + + return 0; +} + +static int checkout_get_actions( + uint32_t **actions_ptr, + size_t **counts_ptr, + checkout_data *data, + git_iterator *workdir) +{ + int error = 0, act; + const git_index_entry *wditem; + git_vector pathspec = GIT_VECTOR_INIT, *deltas; + git_pool pathpool; + git_diff_delta *delta; + size_t i, *counts = NULL; + uint32_t *actions = NULL; + + if (git_pool_init(&pathpool, 1) < 0) + return -1; + + if (data->opts.paths.count > 0 && + git_pathspec__vinit(&pathspec, &data->opts.paths, &pathpool) < 0) + return -1; + + if ((error = git_iterator_current(&wditem, workdir)) < 0 && + error != GIT_ITEROVER) + goto fail; + + deltas = &data->diff->deltas; + + *counts_ptr = counts = git__calloc(CHECKOUT_ACTION__MAX+1, sizeof(size_t)); + *actions_ptr = actions = git__calloc( + deltas->length ? deltas->length : 1, sizeof(uint32_t)); + if (!counts || !actions) { + error = -1; + goto fail; + } + + git_vector_foreach(deltas, i, delta) { + if ((error = checkout_action(&act, data, delta, workdir, &wditem, &pathspec)) == 0) + error = checkout_verify_paths(data->repo, act, delta); + + if (error != 0) + goto fail; + + actions[i] = act; + + if (act & CHECKOUT_ACTION__REMOVE) + counts[CHECKOUT_ACTION__REMOVE]++; + if (act & CHECKOUT_ACTION__UPDATE_BLOB) + counts[CHECKOUT_ACTION__UPDATE_BLOB]++; + if (act & CHECKOUT_ACTION__UPDATE_SUBMODULE) + counts[CHECKOUT_ACTION__UPDATE_SUBMODULE]++; + if (act & CHECKOUT_ACTION__CONFLICT) + counts[CHECKOUT_ACTION__CONFLICT]++; + } + + error = checkout_remaining_wd_items(data, workdir, wditem, &pathspec); + if (error) + goto fail; + + counts[CHECKOUT_ACTION__REMOVE] += data->removes.length; + + if (counts[CHECKOUT_ACTION__CONFLICT] > 0 && + (data->strategy & GIT_CHECKOUT_ALLOW_CONFLICTS) == 0) { + git_error_set(GIT_ERROR_CHECKOUT, "%"PRIuZ" %s checkout", + counts[CHECKOUT_ACTION__CONFLICT], + counts[CHECKOUT_ACTION__CONFLICT] == 1 ? + "conflict prevents" : "conflicts prevent"); + error = GIT_ECONFLICT; + goto fail; + } + + + if ((error = checkout_get_remove_conflicts(data, workdir, &pathspec)) < 0 || + (error = checkout_get_update_conflicts(data, workdir, &pathspec)) < 0) + goto fail; + + counts[CHECKOUT_ACTION__REMOVE_CONFLICT] = git_vector_length(&data->remove_conflicts); + counts[CHECKOUT_ACTION__UPDATE_CONFLICT] = git_vector_length(&data->update_conflicts); + + git_pathspec__vfree(&pathspec); + git_pool_clear(&pathpool); + + return 0; + +fail: + *counts_ptr = NULL; + git__free(counts); + *actions_ptr = NULL; + git__free(actions); + + git_pathspec__vfree(&pathspec); + git_pool_clear(&pathpool); + + return error; +} + +static bool should_remove_existing(checkout_data *data) +{ + int ignorecase; + + if (git_repository__configmap_lookup(&ignorecase, data->repo, GIT_CONFIGMAP_IGNORECASE) < 0) { + ignorecase = 0; + } + + return (ignorecase && + (data->strategy & GIT_CHECKOUT_DONT_REMOVE_EXISTING) == 0); +} + +#define MKDIR_NORMAL \ + GIT_MKDIR_PATH | GIT_MKDIR_VERIFY_DIR +#define MKDIR_REMOVE_EXISTING \ + MKDIR_NORMAL | GIT_MKDIR_REMOVE_FILES | GIT_MKDIR_REMOVE_SYMLINKS + +static int checkout_mkdir( + checkout_data *data, + const char *path, + const char *base, + mode_t mode, + unsigned int flags) +{ + struct git_futils_mkdir_options mkdir_opts = {0}; + int error; + + mkdir_opts.dir_map = data->mkdir_map; + mkdir_opts.pool = &data->pool; + + error = git_futils_mkdir_relative( + path, base, mode, flags, &mkdir_opts); + + data->perfdata.mkdir_calls += mkdir_opts.perfdata.mkdir_calls; + data->perfdata.stat_calls += mkdir_opts.perfdata.stat_calls; + data->perfdata.chmod_calls += mkdir_opts.perfdata.chmod_calls; + + return error; +} + +static int mkpath2file( + checkout_data *data, const char *path, unsigned int mode) +{ + struct stat st; + bool remove_existing = should_remove_existing(data); + unsigned int flags = + (remove_existing ? MKDIR_REMOVE_EXISTING : MKDIR_NORMAL) | + GIT_MKDIR_SKIP_LAST; + int error; + + if ((error = checkout_mkdir( + data, path, data->opts.target_directory, mode, flags)) < 0) + return error; + + if (remove_existing) { + data->perfdata.stat_calls++; + + if (p_lstat(path, &st) == 0) { + + /* Some file, symlink or folder already exists at this name. + * We would have removed it in remove_the_old unless we're on + * a case inensitive filesystem (or the user has asked us not + * to). Remove the similarly named file to write the new. + */ + error = git_futils_rmdir_r(path, NULL, GIT_RMDIR_REMOVE_FILES); + } else if (errno != ENOENT) { + git_error_set(GIT_ERROR_OS, "failed to stat '%s'", path); + return GIT_EEXISTS; + } else { + git_error_clear(); + } + } + + return error; +} + +struct checkout_stream { + git_writestream base; + const char *path; + int fd; + int open; +}; + +static int checkout_stream_write( + git_writestream *s, const char *buffer, size_t len) +{ + struct checkout_stream *stream = (struct checkout_stream *)s; + int ret; + + if ((ret = p_write(stream->fd, buffer, len)) < 0) + git_error_set(GIT_ERROR_OS, "could not write to '%s'", stream->path); + + return ret; +} + +static int checkout_stream_close(git_writestream *s) +{ + struct checkout_stream *stream = (struct checkout_stream *)s; + + GIT_ASSERT_ARG(stream); + GIT_ASSERT_ARG(stream->open); + + stream->open = 0; + return p_close(stream->fd); +} + +static void checkout_stream_free(git_writestream *s) +{ + GIT_UNUSED(s); +} + +static int blob_content_to_file( + checkout_data *data, + struct stat *st, + git_blob *blob, + const char *path, + const char *hint_path, + mode_t entry_filemode) +{ + int flags = data->opts.file_open_flags; + mode_t file_mode = data->opts.file_mode ? + data->opts.file_mode : entry_filemode; + git_filter_session filter_session = GIT_FILTER_SESSION_INIT; + struct checkout_stream writer; + mode_t mode; + git_filter_list *fl = NULL; + int fd; + int error = 0; + + GIT_ASSERT(hint_path != NULL); + + if ((error = mkpath2file(data, path, data->opts.dir_mode)) < 0) + return error; + + if (flags <= 0) + flags = O_CREAT | O_TRUNC | O_WRONLY; + if (!(mode = file_mode)) + mode = GIT_FILEMODE_BLOB; + + if ((fd = p_open(path, flags, mode)) < 0) { + git_error_set(GIT_ERROR_OS, "could not open '%s' for writing", path); + return fd; + } + + filter_session.attr_session = &data->attr_session; + filter_session.temp_buf = &data->tmp; + + if (!data->opts.disable_filters && + (error = git_filter_list__load( + &fl, data->repo, blob, hint_path, + GIT_FILTER_TO_WORKTREE, &filter_session))) { + p_close(fd); + return error; + } + + /* setup the writer */ + memset(&writer, 0, sizeof(struct checkout_stream)); + writer.base.write = checkout_stream_write; + writer.base.close = checkout_stream_close; + writer.base.free = checkout_stream_free; + writer.path = path; + writer.fd = fd; + writer.open = 1; + + error = git_filter_list_stream_blob(fl, blob, &writer.base); + + GIT_ASSERT(writer.open == 0); + + git_filter_list_free(fl); + + if (error < 0) + return error; + + if (st) { + data->perfdata.stat_calls++; + + if ((error = p_stat(path, st)) < 0) { + git_error_set(GIT_ERROR_OS, "failed to stat '%s'", path); + return error; + } + + st->st_mode = entry_filemode; + } + + return 0; +} + +static int blob_content_to_link( + checkout_data *data, + struct stat *st, + git_blob *blob, + const char *path) +{ + git_str linktarget = GIT_STR_INIT; + int error; + + if ((error = mkpath2file(data, path, data->opts.dir_mode)) < 0) + return error; + + if ((error = git_blob__getbuf(&linktarget, blob)) < 0) + return error; + + if (data->can_symlink) { + if ((error = p_symlink(git_str_cstr(&linktarget), path)) < 0) + git_error_set(GIT_ERROR_OS, "could not create symlink %s", path); + } else { + error = git_futils_fake_symlink(git_str_cstr(&linktarget), path); + } + + if (!error) { + data->perfdata.stat_calls++; + + if ((error = p_lstat(path, st)) < 0) + git_error_set(GIT_ERROR_CHECKOUT, "could not stat symlink %s", path); + + st->st_mode = GIT_FILEMODE_LINK; + } + + git_str_dispose(&linktarget); + + return error; +} + +static int checkout_update_index( + checkout_data *data, + const git_diff_file *file, + struct stat *st) +{ + git_index_entry entry; + + if (!data->index) + return 0; + + memset(&entry, 0, sizeof(entry)); + entry.path = (char *)file->path; /* cast to prevent warning */ + git_index_entry__init_from_stat(&entry, st, true); + git_oid_cpy(&entry.id, &file->id); + + return git_index_add(data->index, &entry); +} + +static int checkout_submodule_update_index( + checkout_data *data, + const git_diff_file *file) +{ + git_str *fullpath; + struct stat st; + + /* update the index unless prevented */ + if ((data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) != 0) + return 0; + + if (checkout_target_fullpath(&fullpath, data, file->path) < 0) + return -1; + + data->perfdata.stat_calls++; + if (p_stat(fullpath->ptr, &st) < 0) { + git_error_set( + GIT_ERROR_CHECKOUT, "could not stat submodule %s\n", file->path); + return GIT_ENOTFOUND; + } + + st.st_mode = GIT_FILEMODE_COMMIT; + + return checkout_update_index(data, file, &st); +} + +static int checkout_submodule( + checkout_data *data, + const git_diff_file *file) +{ + bool remove_existing = should_remove_existing(data); + int error = 0; + + /* Until submodules are supported, UPDATE_ONLY means do nothing here */ + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) + return 0; + + if ((error = checkout_mkdir( + data, + file->path, data->opts.target_directory, data->opts.dir_mode, + remove_existing ? MKDIR_REMOVE_EXISTING : MKDIR_NORMAL)) < 0) + return error; + + if ((error = git_submodule_lookup(NULL, data->repo, file->path)) < 0) { + /* I've observed repos with submodules in the tree that do not + * have a .gitmodules - core Git just makes an empty directory + */ + if (error == GIT_ENOTFOUND) { + git_error_clear(); + return checkout_submodule_update_index(data, file); + } + + return error; + } + + /* TODO: Support checkout_strategy options. Two circumstances: + * 1 - submodule already checked out, but we need to move the HEAD + * to the new OID, or + * 2 - submodule not checked out and we should recursively check it out + * + * Checkout will not execute a pull on the submodule, but a clone + * command should probably be able to. Do we need a submodule callback? + */ + + return checkout_submodule_update_index(data, file); +} + +static void report_progress( + checkout_data *data, + const char *path) +{ + if (data->opts.progress_cb) + data->opts.progress_cb( + path, data->completed_steps, data->total_steps, + data->opts.progress_payload); +} + +static int checkout_safe_for_update_only( + checkout_data *data, const char *path, mode_t expected_mode) +{ + struct stat st; + + data->perfdata.stat_calls++; + + if (p_lstat(path, &st) < 0) { + /* if doesn't exist, then no error and no update */ + if (errno == ENOENT || errno == ENOTDIR) + return 0; + + /* otherwise, stat error and no update */ + git_error_set(GIT_ERROR_OS, "failed to stat '%s'", path); + return -1; + } + + /* only safe for update if this is the same type of file */ + if ((st.st_mode & ~0777) == (expected_mode & ~0777)) + return 1; + + return 0; +} + +static int checkout_write_content( + checkout_data *data, + const git_oid *oid, + const char *full_path, + const char *hint_path, + unsigned int mode, + struct stat *st) +{ + int error = 0; + git_blob *blob; + + if ((error = git_blob_lookup(&blob, data->repo, oid)) < 0) + return error; + + if (S_ISLNK(mode)) + error = blob_content_to_link(data, st, blob, full_path); + else + error = blob_content_to_file(data, st, blob, full_path, hint_path, mode); + + git_blob_free(blob); + + /* if we try to create the blob and an existing directory blocks it from + * being written, then there must have been a typechange conflict in a + * parent directory - suppress the error and try to continue. + */ + if ((data->strategy & GIT_CHECKOUT_ALLOW_CONFLICTS) != 0 && + (error == GIT_ENOTFOUND || error == GIT_EEXISTS)) + { + git_error_clear(); + error = 0; + } + + return error; +} + +static int checkout_blob( + checkout_data *data, + const git_diff_file *file) +{ + git_str *fullpath; + struct stat st; + int error = 0; + + if (checkout_target_fullpath(&fullpath, data, file->path) < 0) + return -1; + + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) { + int rval = checkout_safe_for_update_only( + data, fullpath->ptr, file->mode); + + if (rval <= 0) + return rval; + } + + error = checkout_write_content( + data, &file->id, fullpath->ptr, file->path, file->mode, &st); + + /* update the index unless prevented */ + if (!error && (data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0) + error = checkout_update_index(data, file, &st); + + /* update the submodule data if this was a new .gitmodules file */ + if (!error && strcmp(file->path, ".gitmodules") == 0) + data->reload_submodules = true; + + return error; +} + +static int checkout_remove_the_old( + unsigned int *actions, + checkout_data *data) +{ + int error = 0; + git_diff_delta *delta; + const char *str; + size_t i; + git_str *fullpath; + uint32_t flg = GIT_RMDIR_EMPTY_PARENTS | + GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_REMOVE_BLOCKERS; + + if (data->opts.checkout_strategy & GIT_CHECKOUT_SKIP_LOCKED_DIRECTORIES) + flg |= GIT_RMDIR_SKIP_NONEMPTY; + + if (checkout_target_fullpath(&fullpath, data, NULL) < 0) + return -1; + + git_vector_foreach(&data->diff->deltas, i, delta) { + if (actions[i] & CHECKOUT_ACTION__REMOVE) { + error = git_futils_rmdir_r( + delta->old_file.path, fullpath->ptr, flg); + + if (error < 0) + return error; + + data->completed_steps++; + report_progress(data, delta->old_file.path); + + if ((actions[i] & CHECKOUT_ACTION__UPDATE_BLOB) == 0 && + (data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0 && + data->index != NULL) + { + (void)git_index_remove(data->index, delta->old_file.path, 0); + } + } + } + + git_vector_foreach(&data->removes, i, str) { + error = git_futils_rmdir_r(str, fullpath->ptr, flg); + if (error < 0) + return error; + + data->completed_steps++; + report_progress(data, str); + + if ((data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0 && + data->index != NULL) + { + if (str[strlen(str) - 1] == '/') + (void)git_index_remove_directory(data->index, str, 0); + else + (void)git_index_remove(data->index, str, 0); + } + } + + return 0; +} + +static int checkout_create_the_new( + unsigned int *actions, + checkout_data *data) +{ + int error = 0; + git_diff_delta *delta; + size_t i; + + git_vector_foreach(&data->diff->deltas, i, delta) { + if (actions[i] & CHECKOUT_ACTION__UPDATE_BLOB && !S_ISLNK(delta->new_file.mode)) { + if ((error = checkout_blob(data, &delta->new_file)) < 0) + return error; + data->completed_steps++; + report_progress(data, delta->new_file.path); + } + } + + git_vector_foreach(&data->diff->deltas, i, delta) { + if (actions[i] & CHECKOUT_ACTION__UPDATE_BLOB && S_ISLNK(delta->new_file.mode)) { + if ((error = checkout_blob(data, &delta->new_file)) < 0) + return error; + data->completed_steps++; + report_progress(data, delta->new_file.path); + } + } + + return 0; +} + +static int checkout_create_submodules( + unsigned int *actions, + checkout_data *data) +{ + git_diff_delta *delta; + size_t i; + + git_vector_foreach(&data->diff->deltas, i, delta) { + if (actions[i] & CHECKOUT_ACTION__UPDATE_SUBMODULE) { + int error = checkout_submodule(data, &delta->new_file); + if (error < 0) + return error; + + data->completed_steps++; + report_progress(data, delta->new_file.path); + } + } + + return 0; +} + +static int checkout_lookup_head_tree(git_tree **out, git_repository *repo) +{ + int error = 0; + git_reference *ref = NULL; + git_object *head; + + if (!(error = git_repository_head(&ref, repo)) && + !(error = git_reference_peel(&head, ref, GIT_OBJECT_TREE))) + *out = (git_tree *)head; + + git_reference_free(ref); + + return error; +} + + +static int conflict_entry_name( + git_str *out, + const char *side_name, + const char *filename) +{ + if (git_str_puts(out, side_name) < 0 || + git_str_putc(out, ':') < 0 || + git_str_puts(out, filename) < 0) + return -1; + + return 0; +} + +static int checkout_path_suffixed(git_str *path, const char *suffix) +{ + size_t path_len; + int i = 0, error = 0; + + if ((error = git_str_putc(path, '~')) < 0 || (error = git_str_puts(path, suffix)) < 0) + return -1; + + path_len = git_str_len(path); + + while (git_fs_path_exists(git_str_cstr(path)) && i < INT_MAX) { + git_str_truncate(path, path_len); + + if ((error = git_str_putc(path, '_')) < 0 || + (error = git_str_printf(path, "%d", i)) < 0) + return error; + + i++; + } + + if (i == INT_MAX) { + git_str_truncate(path, path_len); + + git_error_set(GIT_ERROR_CHECKOUT, "could not write '%s': working directory file exists", path->ptr); + return GIT_EEXISTS; + } + + return 0; +} + +static int checkout_write_entry( + checkout_data *data, + checkout_conflictdata *conflict, + const git_index_entry *side) +{ + const char *hint_path = NULL, *suffix; + git_str *fullpath; + struct stat st; + int error; + + GIT_ASSERT(side == conflict->ours || side == conflict->theirs); + + if (checkout_target_fullpath(&fullpath, data, side->path) < 0) + return -1; + + if ((conflict->name_collision || conflict->directoryfile) && + (data->strategy & GIT_CHECKOUT_USE_OURS) == 0 && + (data->strategy & GIT_CHECKOUT_USE_THEIRS) == 0) { + + if (side == conflict->ours) + suffix = data->opts.our_label ? data->opts.our_label : + "ours"; + else + suffix = data->opts.their_label ? data->opts.their_label : + "theirs"; + + if (checkout_path_suffixed(fullpath, suffix) < 0) + return -1; + } + + hint_path = side->path; + + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0 && + (error = checkout_safe_for_update_only(data, fullpath->ptr, side->mode)) <= 0) + return error; + + if (!S_ISGITLINK(side->mode)) + return checkout_write_content(data, + &side->id, fullpath->ptr, hint_path, side->mode, &st); + + return 0; +} + +static int checkout_write_entries( + checkout_data *data, + checkout_conflictdata *conflict) +{ + int error = 0; + + if ((error = checkout_write_entry(data, conflict, conflict->ours)) >= 0) + error = checkout_write_entry(data, conflict, conflict->theirs); + + return error; +} + +static int checkout_merge_path( + git_str *out, + checkout_data *data, + checkout_conflictdata *conflict, + git_merge_file_result *result) +{ + const char *our_label_raw, *their_label_raw, *suffix; + int error = 0; + + if ((error = git_str_joinpath(out, data->opts.target_directory, result->path)) < 0 || + (error = git_path_validate_str_length(data->repo, out)) < 0) + return error; + + /* Most conflicts simply use the filename in the index */ + if (!conflict->name_collision) + return 0; + + /* Rename 2->1 conflicts need the branch name appended */ + our_label_raw = data->opts.our_label ? data->opts.our_label : "ours"; + their_label_raw = data->opts.their_label ? data->opts.their_label : "theirs"; + suffix = strcmp(result->path, conflict->ours->path) == 0 ? our_label_raw : their_label_raw; + + if ((error = checkout_path_suffixed(out, suffix)) < 0) + return error; + + return 0; +} + +static int checkout_write_merge( + checkout_data *data, + checkout_conflictdata *conflict) +{ + git_str our_label = GIT_STR_INIT, their_label = GIT_STR_INIT, + path_suffixed = GIT_STR_INIT, path_workdir = GIT_STR_INIT, + in_data = GIT_STR_INIT, out_data = GIT_STR_INIT; + git_merge_file_options opts = GIT_MERGE_FILE_OPTIONS_INIT; + git_merge_file_result result = {0}; + git_filebuf output = GIT_FILEBUF_INIT; + git_filter_list *fl = NULL; + git_filter_session filter_session = GIT_FILTER_SESSION_INIT; + int error = 0; + + if (data->opts.checkout_strategy & GIT_CHECKOUT_CONFLICT_STYLE_DIFF3) + opts.flags |= GIT_MERGE_FILE_STYLE_DIFF3; + + if (data->opts.checkout_strategy & GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3) + opts.flags |= GIT_MERGE_FILE_STYLE_ZDIFF3; + + opts.ancestor_label = data->opts.ancestor_label ? + data->opts.ancestor_label : "ancestor"; + opts.our_label = data->opts.our_label ? + data->opts.our_label : "ours"; + opts.their_label = data->opts.their_label ? + data->opts.their_label : "theirs"; + + /* If all the paths are identical, decorate the diff3 file with the branch + * names. Otherwise, append branch_name:path. + */ + if (conflict->ours && conflict->theirs && + strcmp(conflict->ours->path, conflict->theirs->path) != 0) { + + if ((error = conflict_entry_name( + &our_label, opts.our_label, conflict->ours->path)) < 0 || + (error = conflict_entry_name( + &their_label, opts.their_label, conflict->theirs->path)) < 0) + goto done; + + opts.our_label = git_str_cstr(&our_label); + opts.their_label = git_str_cstr(&their_label); + } + + if ((error = git_merge_file_from_index(&result, data->repo, + conflict->ancestor, conflict->ours, conflict->theirs, &opts)) < 0) + goto done; + + if (result.path == NULL || result.mode == 0) { + git_error_set(GIT_ERROR_CHECKOUT, "could not merge contents of file"); + error = GIT_ECONFLICT; + goto done; + } + + if ((error = checkout_merge_path(&path_workdir, data, conflict, &result)) < 0) + goto done; + + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0 && + (error = checkout_safe_for_update_only(data, git_str_cstr(&path_workdir), result.mode)) <= 0) + goto done; + + if (!data->opts.disable_filters) { + in_data.ptr = (char *)result.ptr; + in_data.size = result.len; + + filter_session.attr_session = &data->attr_session; + filter_session.temp_buf = &data->tmp; + + if ((error = git_filter_list__load( + &fl, data->repo, NULL, result.path, + GIT_FILTER_TO_WORKTREE, &filter_session)) < 0 || + (error = git_filter_list__convert_buf(&out_data, fl, &in_data)) < 0) + goto done; + } else { + out_data.ptr = (char *)result.ptr; + out_data.size = result.len; + } + + if ((error = mkpath2file(data, path_workdir.ptr, data->opts.dir_mode)) < 0 || + (error = git_filebuf_open(&output, git_str_cstr(&path_workdir), GIT_FILEBUF_DO_NOT_BUFFER, result.mode)) < 0 || + (error = git_filebuf_write(&output, out_data.ptr, out_data.size)) < 0 || + (error = git_filebuf_commit(&output)) < 0) + goto done; + +done: + git_filter_list_free(fl); + + git_str_dispose(&out_data); + git_str_dispose(&our_label); + git_str_dispose(&their_label); + + git_merge_file_result_free(&result); + git_str_dispose(&path_workdir); + git_str_dispose(&path_suffixed); + + return error; +} + +static int checkout_conflict_add( + checkout_data *data, + const git_index_entry *conflict) +{ + int error = git_index_remove(data->index, conflict->path, 0); + + if (error == GIT_ENOTFOUND) + git_error_clear(); + else if (error < 0) + return error; + + return git_index_add(data->index, conflict); +} + +static int checkout_conflict_update_index( + checkout_data *data, + checkout_conflictdata *conflict) +{ + int error = 0; + + if (conflict->ancestor) + error = checkout_conflict_add(data, conflict->ancestor); + + if (!error && conflict->ours) + error = checkout_conflict_add(data, conflict->ours); + + if (!error && conflict->theirs) + error = checkout_conflict_add(data, conflict->theirs); + + return error; +} + +static int checkout_create_conflicts(checkout_data *data) +{ + checkout_conflictdata *conflict; + size_t i; + int error = 0; + + git_vector_foreach(&data->update_conflicts, i, conflict) { + + /* Both deleted: nothing to do */ + if (conflict->ours == NULL && conflict->theirs == NULL) + error = 0; + + else if ((data->strategy & GIT_CHECKOUT_USE_OURS) && + conflict->ours) + error = checkout_write_entry(data, conflict, conflict->ours); + else if ((data->strategy & GIT_CHECKOUT_USE_THEIRS) && + conflict->theirs) + error = checkout_write_entry(data, conflict, conflict->theirs); + + /* Ignore the other side of name collisions. */ + else if ((data->strategy & GIT_CHECKOUT_USE_OURS) && + !conflict->ours && conflict->name_collision) + error = 0; + else if ((data->strategy & GIT_CHECKOUT_USE_THEIRS) && + !conflict->theirs && conflict->name_collision) + error = 0; + + /* For modify/delete, name collisions and d/f conflicts, write + * the file (potentially with the name mangled. + */ + else if (conflict->ours != NULL && conflict->theirs == NULL) + error = checkout_write_entry(data, conflict, conflict->ours); + else if (conflict->ours == NULL && conflict->theirs != NULL) + error = checkout_write_entry(data, conflict, conflict->theirs); + + /* Add/add conflicts and rename 1->2 conflicts, write the + * ours/theirs sides (potentially name mangled). + */ + else if (conflict->one_to_two) + error = checkout_write_entries(data, conflict); + + /* If all sides are links, write the ours side */ + else if (S_ISLNK(conflict->ours->mode) && + S_ISLNK(conflict->theirs->mode)) + error = checkout_write_entry(data, conflict, conflict->ours); + /* Link/file conflicts, write the file side */ + else if (S_ISLNK(conflict->ours->mode)) + error = checkout_write_entry(data, conflict, conflict->theirs); + else if (S_ISLNK(conflict->theirs->mode)) + error = checkout_write_entry(data, conflict, conflict->ours); + + /* If any side is a gitlink, do nothing. */ + else if (conflict->submodule) + error = 0; + + /* If any side is binary, write the ours side */ + else if (conflict->binary) + error = checkout_write_entry(data, conflict, conflict->ours); + + else if (!error) + error = checkout_write_merge(data, conflict); + + /* Update the index extensions (REUC and NAME) if we're checking + * out a different index. (Otherwise just leave them there.) + */ + if (!error && (data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0) + error = checkout_conflict_update_index(data, conflict); + + if (error) + break; + + data->completed_steps++; + report_progress(data, + conflict->ours ? conflict->ours->path : + (conflict->theirs ? conflict->theirs->path : conflict->ancestor->path)); + } + + return error; +} + +static int checkout_remove_conflicts(checkout_data *data) +{ + const char *conflict; + size_t i; + + git_vector_foreach(&data->remove_conflicts, i, conflict) { + if (git_index_conflict_remove(data->index, conflict) < 0) + return -1; + + data->completed_steps++; + } + + return 0; +} + +static int checkout_extensions_update_index(checkout_data *data) +{ + const git_index_reuc_entry *reuc_entry; + const git_index_name_entry *name_entry; + size_t i; + int error = 0; + + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) + return 0; + + if (data->update_reuc) { + git_vector_foreach(data->update_reuc, i, reuc_entry) { + if ((error = git_index_reuc_add(data->index, reuc_entry->path, + reuc_entry->mode[0], &reuc_entry->oid[0], + reuc_entry->mode[1], &reuc_entry->oid[1], + reuc_entry->mode[2], &reuc_entry->oid[2])) < 0) + goto done; + } + } + + if (data->update_names) { + git_vector_foreach(data->update_names, i, name_entry) { + if ((error = git_index_name_add(data->index, name_entry->ancestor, + name_entry->ours, name_entry->theirs)) < 0) + goto done; + } + } + +done: + return error; +} + +static void checkout_data_clear(checkout_data *data) +{ + if (data->opts_free_baseline) { + git_tree_free(data->opts.baseline); + data->opts.baseline = NULL; + } + + git_vector_free(&data->removes); + git_pool_clear(&data->pool); + + git_vector_free_deep(&data->remove_conflicts); + git_vector_free_deep(&data->update_conflicts); + + git__free(data->pfx); + data->pfx = NULL; + + git_str_dispose(&data->target_path); + git_str_dispose(&data->tmp); + + git_index_free(data->index); + data->index = NULL; + + git_strmap_free(data->mkdir_map); + data->mkdir_map = NULL; + + git_attr_session__free(&data->attr_session); +} + +static int validate_target_directory(checkout_data *data) +{ + int error; + + if ((error = git_path_validate_length(data->repo, data->opts.target_directory)) < 0) + return error; + + if (git_fs_path_isdir(data->opts.target_directory)) + return 0; + + error = checkout_mkdir(data, data->opts.target_directory, NULL, + GIT_DIR_MODE, GIT_MKDIR_VERIFY_DIR); + + return error; +} + +static int checkout_data_init( + checkout_data *data, + git_iterator *target, + const git_checkout_options *proposed) +{ + int error = 0; + git_repository *repo = git_iterator_owner(target); + + memset(data, 0, sizeof(*data)); + + if (!repo) { + git_error_set(GIT_ERROR_CHECKOUT, "cannot checkout nothing"); + return -1; + } + + if ((!proposed || !proposed->target_directory) && + (error = git_repository__ensure_not_bare(repo, "checkout")) < 0) + return error; + + data->repo = repo; + data->target = target; + + GIT_ERROR_CHECK_VERSION( + proposed, GIT_CHECKOUT_OPTIONS_VERSION, "git_checkout_options"); + + if (!proposed) + GIT_INIT_STRUCTURE(&data->opts, GIT_CHECKOUT_OPTIONS_VERSION); + else + memmove(&data->opts, proposed, sizeof(git_checkout_options)); + + if (!data->opts.target_directory) + data->opts.target_directory = git_repository_workdir(repo); + else if ((error = validate_target_directory(data)) < 0) + goto cleanup; + + if ((error = git_repository_index(&data->index, data->repo)) < 0) + goto cleanup; + + /* refresh config and index content unless NO_REFRESH is given */ + if ((data->opts.checkout_strategy & GIT_CHECKOUT_NO_REFRESH) == 0) { + git_config *cfg; + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + goto cleanup; + + /* Reload the repository index (unless we're checking out the + * index; then it has the changes we're trying to check out + * and those should not be overwritten.) + */ + if (data->index != git_iterator_index(target)) { + if (data->opts.checkout_strategy & GIT_CHECKOUT_FORCE) { + /* When forcing, we can blindly re-read the index */ + if ((error = git_index_read(data->index, false)) < 0) + goto cleanup; + } else { + /* + * When not being forced, we need to check for unresolved + * conflicts and unsaved changes in the index before + * proceeding. + */ + if (git_index_has_conflicts(data->index)) { + error = GIT_ECONFLICT; + git_error_set(GIT_ERROR_CHECKOUT, + "unresolved conflicts exist in the index"); + goto cleanup; + } + + if ((error = git_index_read_safely(data->index)) < 0) + goto cleanup; + } + + /* clean conflict data in the current index */ + git_index_name_clear(data->index); + git_index_reuc_clear(data->index); + } + } + + /* if you are forcing, allow all safe updates, plus recreate missing */ + if ((data->opts.checkout_strategy & GIT_CHECKOUT_FORCE) != 0) + data->opts.checkout_strategy |= GIT_CHECKOUT_SAFE | + GIT_CHECKOUT_RECREATE_MISSING; + + /* if the repository does not actually have an index file, then this + * is an initial checkout (perhaps from clone), so we allow safe updates + */ + if (!data->index->on_disk && + (data->opts.checkout_strategy & GIT_CHECKOUT_SAFE) != 0) + data->opts.checkout_strategy |= GIT_CHECKOUT_RECREATE_MISSING; + + data->strategy = data->opts.checkout_strategy; + + /* opts->disable_filters is false by default */ + + if (!data->opts.dir_mode) + data->opts.dir_mode = GIT_DIR_MODE; + + if (!data->opts.file_open_flags) + data->opts.file_open_flags = O_CREAT | O_TRUNC | O_WRONLY; + + data->pfx = git_pathspec_prefix(&data->opts.paths); + + if ((error = git_repository__configmap_lookup( + &data->can_symlink, repo, GIT_CONFIGMAP_SYMLINKS)) < 0) + goto cleanup; + + if ((error = git_repository__configmap_lookup( + &data->respect_filemode, repo, GIT_CONFIGMAP_FILEMODE)) < 0) + goto cleanup; + + if (!data->opts.baseline && !data->opts.baseline_index) { + data->opts_free_baseline = true; + error = 0; + + /* if we don't have an index, this is an initial checkout and + * should be against an empty baseline + */ + if (data->index->on_disk) + error = checkout_lookup_head_tree(&data->opts.baseline, repo); + + if (error == GIT_EUNBORNBRANCH) { + error = 0; + git_error_clear(); + } + + if (error < 0) + goto cleanup; + } + + if ((data->opts.checkout_strategy & + (GIT_CHECKOUT_CONFLICT_STYLE_MERGE | GIT_CHECKOUT_CONFLICT_STYLE_DIFF3)) == 0) { + git_config_entry *conflict_style = NULL; + git_config *cfg = NULL; + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0 || + (error = git_config_get_entry(&conflict_style, cfg, "merge.conflictstyle")) < 0 || + error == GIT_ENOTFOUND) + ; + else if (error) + goto cleanup; + else if (strcmp(conflict_style->value, "merge") == 0) + data->opts.checkout_strategy |= GIT_CHECKOUT_CONFLICT_STYLE_MERGE; + else if (strcmp(conflict_style->value, "diff3") == 0) + data->opts.checkout_strategy |= GIT_CHECKOUT_CONFLICT_STYLE_DIFF3; + else if (strcmp(conflict_style->value, "zdiff3") == 0) + data->opts.checkout_strategy |= GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3; + else { + git_error_set(GIT_ERROR_CHECKOUT, "unknown style '%s' given for 'merge.conflictstyle'", + conflict_style->value); + error = -1; + git_config_entry_free(conflict_style); + goto cleanup; + } + git_config_entry_free(conflict_style); + } + + if ((error = git_pool_init(&data->pool, 1)) < 0 || + (error = git_vector_init(&data->removes, 0, git__strcmp_cb)) < 0 || + (error = git_vector_init(&data->remove_conflicts, 0, NULL)) < 0 || + (error = git_vector_init(&data->update_conflicts, 0, NULL)) < 0 || + (error = git_str_puts(&data->target_path, data->opts.target_directory)) < 0 || + (error = git_fs_path_to_dir(&data->target_path)) < 0 || + (error = git_strmap_new(&data->mkdir_map)) < 0) + goto cleanup; + + data->target_len = git_str_len(&data->target_path); + + git_attr_session__init(&data->attr_session, data->repo); + +cleanup: + if (error < 0) + checkout_data_clear(data); + + return error; +} + +#define CHECKOUT_INDEX_DONT_WRITE_MASK \ + (GIT_CHECKOUT_DONT_UPDATE_INDEX | GIT_CHECKOUT_DONT_WRITE_INDEX) + +GIT_INLINE(void) setup_pathspecs( + git_iterator_options *iter_opts, + const git_checkout_options *checkout_opts) +{ + if (checkout_opts && + (checkout_opts->checkout_strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH)) { + iter_opts->pathlist.count = checkout_opts->paths.count; + iter_opts->pathlist.strings = checkout_opts->paths.strings; + } +} + +int git_checkout_iterator( + git_iterator *target, + git_index *index, + const git_checkout_options *opts) +{ + int error = 0; + git_iterator *baseline = NULL, *workdir = NULL; + git_iterator_options baseline_opts = GIT_ITERATOR_OPTIONS_INIT, + workdir_opts = GIT_ITERATOR_OPTIONS_INIT; + checkout_data data = {0}; + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + uint32_t *actions = NULL; + size_t *counts = NULL; + + /* initialize structures and options */ + error = checkout_data_init(&data, target, opts); + if (error < 0) + return error; + + diff_opts.flags = + GIT_DIFF_INCLUDE_UNMODIFIED | + GIT_DIFF_INCLUDE_UNREADABLE | + GIT_DIFF_INCLUDE_UNTRACKED | + GIT_DIFF_RECURSE_UNTRACKED_DIRS | /* needed to match baseline */ + GIT_DIFF_INCLUDE_IGNORED | + GIT_DIFF_INCLUDE_TYPECHANGE | + GIT_DIFF_INCLUDE_TYPECHANGE_TREES | + GIT_DIFF_SKIP_BINARY_CHECK | + GIT_DIFF_INCLUDE_CASECHANGE; + if (data.opts.checkout_strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) + diff_opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH; + if (data.opts.paths.count > 0) + diff_opts.pathspec = data.opts.paths; + + /* set up iterators */ + + workdir_opts.flags = git_iterator_ignore_case(target) ? + GIT_ITERATOR_IGNORE_CASE : GIT_ITERATOR_DONT_IGNORE_CASE; + workdir_opts.flags |= GIT_ITERATOR_DONT_AUTOEXPAND; + workdir_opts.start = data.pfx; + workdir_opts.end = data.pfx; + + setup_pathspecs(&workdir_opts, opts); + + if ((error = git_iterator_reset_range(target, data.pfx, data.pfx)) < 0 || + (error = git_iterator_for_workdir_ext( + &workdir, data.repo, data.opts.target_directory, index, NULL, + &workdir_opts)) < 0) + goto cleanup; + + baseline_opts.flags = git_iterator_ignore_case(target) ? + GIT_ITERATOR_IGNORE_CASE : GIT_ITERATOR_DONT_IGNORE_CASE; + baseline_opts.start = data.pfx; + baseline_opts.end = data.pfx; + + setup_pathspecs(&baseline_opts, opts); + + if (data.opts.baseline_index) { + if ((error = git_iterator_for_index( + &baseline, git_index_owner(data.opts.baseline_index), + data.opts.baseline_index, &baseline_opts)) < 0) + goto cleanup; + } else { + if ((error = git_iterator_for_tree( + &baseline, data.opts.baseline, &baseline_opts)) < 0) + goto cleanup; + } + + /* Should not have case insensitivity mismatch */ + GIT_ASSERT(git_iterator_ignore_case(workdir) == git_iterator_ignore_case(baseline)); + + /* Generate baseline-to-target diff which will include an entry for + * every possible update that might need to be made. + */ + if ((error = git_diff__from_iterators( + &data.diff, data.repo, baseline, target, &diff_opts)) < 0) + goto cleanup; + + /* Loop through diff (and working directory iterator) building a list of + * actions to be taken, plus look for conflicts and send notifications, + * then loop through conflicts. + */ + if ((error = checkout_get_actions(&actions, &counts, &data, workdir)) != 0) + goto cleanup; + + if (data.strategy & GIT_CHECKOUT_DRY_RUN) + goto cleanup; + + data.total_steps = counts[CHECKOUT_ACTION__REMOVE] + + counts[CHECKOUT_ACTION__REMOVE_CONFLICT] + + counts[CHECKOUT_ACTION__UPDATE_BLOB] + + counts[CHECKOUT_ACTION__UPDATE_SUBMODULE] + + counts[CHECKOUT_ACTION__UPDATE_CONFLICT]; + + report_progress(&data, NULL); /* establish 0 baseline */ + + /* To deal with some order dependencies, perform remaining checkout + * in three passes: removes, then update blobs, then update submodules. + */ + if (counts[CHECKOUT_ACTION__REMOVE] > 0 && + (error = checkout_remove_the_old(actions, &data)) < 0) + goto cleanup; + + if (counts[CHECKOUT_ACTION__REMOVE_CONFLICT] > 0 && + (error = checkout_remove_conflicts(&data)) < 0) + goto cleanup; + + if (counts[CHECKOUT_ACTION__UPDATE_BLOB] > 0 && + (error = checkout_create_the_new(actions, &data)) < 0) + goto cleanup; + + if (counts[CHECKOUT_ACTION__UPDATE_SUBMODULE] > 0 && + (error = checkout_create_submodules(actions, &data)) < 0) + goto cleanup; + + if (counts[CHECKOUT_ACTION__UPDATE_CONFLICT] > 0 && + (error = checkout_create_conflicts(&data)) < 0) + goto cleanup; + + if (data.index != git_iterator_index(target) && + (error = checkout_extensions_update_index(&data)) < 0) + goto cleanup; + + GIT_ASSERT(data.completed_steps == data.total_steps); + + if (data.opts.perfdata_cb) + data.opts.perfdata_cb(&data.perfdata, data.opts.perfdata_payload); + +cleanup: + if (!error && data.index != NULL && + (data.strategy & CHECKOUT_INDEX_DONT_WRITE_MASK) == 0) + error = git_index_write(data.index); + + git_diff_free(data.diff); + git_iterator_free(workdir); + git_iterator_free(baseline); + git__free(actions); + git__free(counts); + checkout_data_clear(&data); + + return error; +} + +int git_checkout_index( + git_repository *repo, + git_index *index, + const git_checkout_options *opts) +{ + git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT; + int error, owned = 0; + git_iterator *index_i; + + if (!index && !repo) { + git_error_set(GIT_ERROR_CHECKOUT, + "must provide either repository or index to checkout"); + return -1; + } + + if (index && repo && + git_index_owner(index) && + git_index_owner(index) != repo) { + git_error_set(GIT_ERROR_CHECKOUT, + "index to checkout does not match repository"); + return -1; + } else if(index && repo && !git_index_owner(index)) { + GIT_REFCOUNT_OWN(index, repo); + owned = 1; + } + + if (!repo) + repo = git_index_owner(index); + + if (!index && (error = git_repository_index__weakptr(&index, repo)) < 0) + return error; + GIT_REFCOUNT_INC(index); + + setup_pathspecs(&iter_opts, opts); + + if (!(error = git_iterator_for_index(&index_i, repo, index, &iter_opts))) + error = git_checkout_iterator(index_i, index, opts); + + if (owned) + GIT_REFCOUNT_OWN(index, NULL); + + git_iterator_free(index_i); + git_index_free(index); + + return error; +} + +int git_checkout_tree( + git_repository *repo, + const git_object *treeish, + const git_checkout_options *opts) +{ + int error; + git_index *index; + git_tree *tree = NULL; + git_iterator *tree_i = NULL; + git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT; + + if (!treeish && !repo) { + git_error_set(GIT_ERROR_CHECKOUT, + "must provide either repository or tree to checkout"); + return -1; + } + if (treeish && repo && git_object_owner(treeish) != repo) { + git_error_set(GIT_ERROR_CHECKOUT, + "object to checkout does not match repository"); + return -1; + } + + if (!repo) + repo = git_object_owner(treeish); + + if (treeish) { + if (git_object_peel((git_object **)&tree, treeish, GIT_OBJECT_TREE) < 0) { + git_error_set( + GIT_ERROR_CHECKOUT, "provided object cannot be peeled to a tree"); + return -1; + } + } + else { + if ((error = checkout_lookup_head_tree(&tree, repo)) < 0) { + if (error != GIT_EUNBORNBRANCH) + git_error_set( + GIT_ERROR_CHECKOUT, + "HEAD could not be peeled to a tree and no treeish given"); + return error; + } + } + + if ((error = git_repository_index(&index, repo)) < 0) + return error; + + setup_pathspecs(&iter_opts, opts); + + if (!(error = git_iterator_for_tree(&tree_i, tree, &iter_opts))) + error = git_checkout_iterator(tree_i, index, opts); + + git_iterator_free(tree_i); + git_index_free(index); + git_tree_free(tree); + + return error; +} + +int git_checkout_head( + git_repository *repo, + const git_checkout_options *opts) +{ + GIT_ASSERT_ARG(repo); + + return git_checkout_tree(repo, NULL, opts); +} + +int git_checkout_options_init(git_checkout_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_checkout_options, GIT_CHECKOUT_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_checkout_init_options(git_checkout_options *opts, unsigned int version) +{ + return git_checkout_options_init(opts, version); +} +#endif diff --git a/src/libgit2/checkout.h b/src/libgit2/checkout.h new file mode 100644 index 0000000..517fbf3 --- /dev/null +++ b/src/libgit2/checkout.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_checkout_h__ +#define INCLUDE_checkout_h__ + +#include "common.h" + +#include "git2/checkout.h" +#include "iterator.h" + +#define GIT_CHECKOUT__NOTIFY_CONFLICT_TREE (1u << 12) + +/** + * Update the working directory to match the target iterator. The + * expected baseline value can be passed in via the checkout options + * or else will default to the HEAD commit. + */ +extern int git_checkout_iterator( + git_iterator *target, + git_index *index, + const git_checkout_options *opts); + +#endif diff --git a/src/libgit2/cherrypick.c b/src/libgit2/cherrypick.c new file mode 100644 index 0000000..3ef42d5 --- /dev/null +++ b/src/libgit2/cherrypick.c @@ -0,0 +1,242 @@ +/* +* Copyright (C) the libgit2 contributors. All rights reserved. +* +* This file is part of libgit2, distributed under the GNU GPL v2 with +* a Linking Exception. For full terms see the included COPYING file. +*/ + +#include "common.h" + +#include "repository.h" +#include "filebuf.h" +#include "merge.h" +#include "vector.h" +#include "index.h" + +#include "git2/types.h" +#include "git2/merge.h" +#include "git2/cherrypick.h" +#include "git2/commit.h" +#include "git2/sys/commit.h" + +#define GIT_CHERRYPICK_FILE_MODE 0666 + +static int write_cherrypick_head( + git_repository *repo, + const char *commit_oidstr) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_str file_path = GIT_STR_INIT; + int error = 0; + + if ((error = git_str_joinpath(&file_path, repo->gitdir, GIT_CHERRYPICK_HEAD_FILE)) >= 0 && + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_CREATE_LEADING_DIRS, GIT_CHERRYPICK_FILE_MODE)) >= 0 && + (error = git_filebuf_printf(&file, "%s\n", commit_oidstr)) >= 0) + error = git_filebuf_commit(&file); + + if (error < 0) + git_filebuf_cleanup(&file); + + git_str_dispose(&file_path); + + return error; +} + +static int write_merge_msg( + git_repository *repo, + const char *commit_msg) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_str file_path = GIT_STR_INIT; + int error = 0; + + if ((error = git_str_joinpath(&file_path, repo->gitdir, GIT_MERGE_MSG_FILE)) < 0 || + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_CREATE_LEADING_DIRS, GIT_CHERRYPICK_FILE_MODE)) < 0 || + (error = git_filebuf_printf(&file, "%s", commit_msg)) < 0) + goto cleanup; + + error = git_filebuf_commit(&file); + +cleanup: + if (error < 0) + git_filebuf_cleanup(&file); + + git_str_dispose(&file_path); + + return error; +} + +static int cherrypick_normalize_opts( + git_repository *repo, + git_cherrypick_options *opts, + const git_cherrypick_options *given, + const char *their_label) +{ + int error = 0; + unsigned int default_checkout_strategy = GIT_CHECKOUT_SAFE | + GIT_CHECKOUT_ALLOW_CONFLICTS; + + GIT_UNUSED(repo); + + if (given != NULL) + memcpy(opts, given, sizeof(git_cherrypick_options)); + else { + git_cherrypick_options default_opts = GIT_CHERRYPICK_OPTIONS_INIT; + memcpy(opts, &default_opts, sizeof(git_cherrypick_options)); + } + + if (!opts->checkout_opts.checkout_strategy) + opts->checkout_opts.checkout_strategy = default_checkout_strategy; + + if (!opts->checkout_opts.our_label) + opts->checkout_opts.our_label = "HEAD"; + + if (!opts->checkout_opts.their_label) + opts->checkout_opts.their_label = their_label; + + return error; +} + +static int cherrypick_state_cleanup(git_repository *repo) +{ + const char *state_files[] = { GIT_CHERRYPICK_HEAD_FILE, GIT_MERGE_MSG_FILE }; + + return git_repository__cleanup_files(repo, state_files, ARRAY_SIZE(state_files)); +} + +static int cherrypick_seterr(git_commit *commit, const char *fmt) +{ + char commit_oidstr[GIT_OID_MAX_HEXSIZE + 1]; + + git_error_set(GIT_ERROR_CHERRYPICK, fmt, + git_oid_tostr(commit_oidstr, GIT_OID_MAX_HEXSIZE + 1, git_commit_id(commit))); + + return -1; +} + +int git_cherrypick_commit( + git_index **out, + git_repository *repo, + git_commit *cherrypick_commit, + git_commit *our_commit, + unsigned int mainline, + const git_merge_options *merge_opts) +{ + git_commit *parent_commit = NULL; + git_tree *parent_tree = NULL, *our_tree = NULL, *cherrypick_tree = NULL; + int parent = 0, error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(cherrypick_commit); + GIT_ASSERT_ARG(our_commit); + + if (git_commit_parentcount(cherrypick_commit) > 1) { + if (!mainline) + return cherrypick_seterr(cherrypick_commit, + "mainline branch is not specified but %s is a merge commit"); + + parent = mainline; + } else { + if (mainline) + return cherrypick_seterr(cherrypick_commit, + "mainline branch specified but %s is not a merge commit"); + + parent = git_commit_parentcount(cherrypick_commit); + } + + if (parent && + ((error = git_commit_parent(&parent_commit, cherrypick_commit, (parent - 1))) < 0 || + (error = git_commit_tree(&parent_tree, parent_commit)) < 0)) + goto done; + + if ((error = git_commit_tree(&cherrypick_tree, cherrypick_commit)) < 0 || + (error = git_commit_tree(&our_tree, our_commit)) < 0) + goto done; + + error = git_merge_trees(out, repo, parent_tree, our_tree, cherrypick_tree, merge_opts); + +done: + git_tree_free(parent_tree); + git_tree_free(our_tree); + git_tree_free(cherrypick_tree); + git_commit_free(parent_commit); + + return error; +} + +int git_cherrypick( + git_repository *repo, + git_commit *commit, + const git_cherrypick_options *given_opts) +{ + git_cherrypick_options opts; + git_reference *our_ref = NULL; + git_commit *our_commit = NULL; + char commit_oidstr[GIT_OID_MAX_HEXSIZE + 1]; + const char *commit_msg, *commit_summary; + git_str their_label = GIT_STR_INIT; + git_index *index = NULL; + git_indexwriter indexwriter = GIT_INDEXWRITER_INIT; + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(commit); + + GIT_ERROR_CHECK_VERSION(given_opts, GIT_CHERRYPICK_OPTIONS_VERSION, "git_cherrypick_options"); + + if ((error = git_repository__ensure_not_bare(repo, "cherry-pick")) < 0) + return error; + + if ((commit_msg = git_commit_message(commit)) == NULL || + (commit_summary = git_commit_summary(commit)) == NULL) { + error = -1; + goto on_error; + } + + git_oid_nfmt(commit_oidstr, sizeof(commit_oidstr), git_commit_id(commit)); + + if ((error = write_merge_msg(repo, commit_msg)) < 0 || + (error = git_str_printf(&their_label, "%.7s... %s", commit_oidstr, commit_summary)) < 0 || + (error = cherrypick_normalize_opts(repo, &opts, given_opts, git_str_cstr(&their_label))) < 0 || + (error = git_indexwriter_init_for_operation(&indexwriter, repo, &opts.checkout_opts.checkout_strategy)) < 0 || + (error = write_cherrypick_head(repo, commit_oidstr)) < 0 || + (error = git_repository_head(&our_ref, repo)) < 0 || + (error = git_reference_peel((git_object **)&our_commit, our_ref, GIT_OBJECT_COMMIT)) < 0 || + (error = git_cherrypick_commit(&index, repo, commit, our_commit, opts.mainline, &opts.merge_opts)) < 0 || + (error = git_merge__check_result(repo, index)) < 0 || + (error = git_merge__append_conflicts_to_merge_msg(repo, index)) < 0 || + (error = git_checkout_index(repo, index, &opts.checkout_opts)) < 0 || + (error = git_indexwriter_commit(&indexwriter)) < 0) + goto on_error; + + goto done; + +on_error: + cherrypick_state_cleanup(repo); + +done: + git_indexwriter_cleanup(&indexwriter); + git_index_free(index); + git_commit_free(our_commit); + git_reference_free(our_ref); + git_str_dispose(&their_label); + + return error; +} + +int git_cherrypick_options_init( + git_cherrypick_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_cherrypick_options, GIT_CHERRYPICK_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_cherrypick_init_options( + git_cherrypick_options *opts, unsigned int version) +{ + return git_cherrypick_options_init(opts, version); +} +#endif diff --git a/src/libgit2/clone.c b/src/libgit2/clone.c new file mode 100644 index 0000000..fca0ca0 --- /dev/null +++ b/src/libgit2/clone.c @@ -0,0 +1,686 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "clone.h" + +#include "git2/clone.h" +#include "git2/remote.h" +#include "git2/revparse.h" +#include "git2/branch.h" +#include "git2/config.h" +#include "git2/checkout.h" +#include "git2/commit.h" +#include "git2/tree.h" + +#include "remote.h" +#include "futils.h" +#include "refs.h" +#include "fs_path.h" +#include "repository.h" +#include "odb.h" + +static int clone_local_into(git_repository *repo, git_remote *remote, const git_fetch_options *fetch_opts, const git_checkout_options *co_opts, const char *branch, int link); + +static int create_branch( + git_reference **branch, + git_repository *repo, + const git_oid *target, + const char *name, + const char *log_message) +{ + git_commit *head_obj = NULL; + git_reference *branch_ref = NULL; + git_str refname = GIT_STR_INIT; + int error; + + /* Find the target commit */ + if ((error = git_commit_lookup(&head_obj, repo, target)) < 0) + return error; + + /* Create the new branch */ + if ((error = git_str_printf(&refname, GIT_REFS_HEADS_DIR "%s", name)) < 0) + return error; + + error = git_reference_create(&branch_ref, repo, git_str_cstr(&refname), target, 0, log_message); + git_str_dispose(&refname); + git_commit_free(head_obj); + + if (!error) + *branch = branch_ref; + else + git_reference_free(branch_ref); + + return error; +} + +static int setup_tracking_config( + git_repository *repo, + const char *branch_name, + const char *remote_name, + const char *merge_target) +{ + git_config *cfg; + git_str remote_key = GIT_STR_INIT, merge_key = GIT_STR_INIT; + int error = -1; + + if (git_repository_config__weakptr(&cfg, repo) < 0) + return -1; + + if (git_str_printf(&remote_key, "branch.%s.remote", branch_name) < 0) + goto cleanup; + + if (git_str_printf(&merge_key, "branch.%s.merge", branch_name) < 0) + goto cleanup; + + if (git_config_set_string(cfg, git_str_cstr(&remote_key), remote_name) < 0) + goto cleanup; + + if (git_config_set_string(cfg, git_str_cstr(&merge_key), merge_target) < 0) + goto cleanup; + + error = 0; + +cleanup: + git_str_dispose(&remote_key); + git_str_dispose(&merge_key); + return error; +} + +static int create_tracking_branch( + git_reference **branch, + git_repository *repo, + const git_oid *target, + const char *branch_name, + const char *log_message) +{ + int error; + + if ((error = create_branch(branch, repo, target, branch_name, log_message)) < 0) + return error; + + return setup_tracking_config( + repo, + branch_name, + GIT_REMOTE_ORIGIN, + git_reference_name(*branch)); +} + +static int update_head_to_new_branch( + git_repository *repo, + const git_oid *target, + const char *name, + const char *reflog_message) +{ + git_reference *tracking_branch = NULL; + int error; + + if (!git__prefixcmp(name, GIT_REFS_HEADS_DIR)) + name += strlen(GIT_REFS_HEADS_DIR); + + error = create_tracking_branch(&tracking_branch, repo, target, name, + reflog_message); + + if (!error) + error = git_repository_set_head( + repo, git_reference_name(tracking_branch)); + + git_reference_free(tracking_branch); + + /* if it already existed, then the user's refspec created it for us, ignore it' */ + if (error == GIT_EEXISTS) + error = 0; + + return error; +} + +static int update_head_to_default(git_repository *repo) +{ + git_str initialbranch = GIT_STR_INIT; + const char *branch_name; + int error = 0; + + if ((error = git_repository_initialbranch(&initialbranch, repo)) < 0) + goto done; + + if (git__prefixcmp(initialbranch.ptr, GIT_REFS_HEADS_DIR) != 0) { + git_error_set(GIT_ERROR_INVALID, "invalid initial branch '%s'", initialbranch.ptr); + error = -1; + goto done; + } + + branch_name = initialbranch.ptr + strlen(GIT_REFS_HEADS_DIR); + + error = setup_tracking_config(repo, branch_name, GIT_REMOTE_ORIGIN, + initialbranch.ptr); + +done: + git_str_dispose(&initialbranch); + return error; +} + +static int update_remote_head( + git_repository *repo, + git_remote *remote, + git_str *target, + const char *reflog_message) +{ + git_refspec *refspec; + git_reference *remote_head = NULL; + git_str remote_head_name = GIT_STR_INIT; + git_str remote_branch_name = GIT_STR_INIT; + int error; + + /* Determine the remote tracking ref name from the local branch */ + refspec = git_remote__matching_refspec(remote, git_str_cstr(target)); + + if (refspec == NULL) { + git_error_set(GIT_ERROR_NET, "the remote's default branch does not fit the refspec configuration"); + error = GIT_EINVALIDSPEC; + goto cleanup; + } + + if ((error = git_refspec__transform( + &remote_branch_name, + refspec, + git_str_cstr(target))) < 0) + goto cleanup; + + if ((error = git_str_printf(&remote_head_name, + "%s%s/%s", + GIT_REFS_REMOTES_DIR, + git_remote_name(remote), + GIT_HEAD_FILE)) < 0) + goto cleanup; + + error = git_reference_symbolic_create( + &remote_head, + repo, + git_str_cstr(&remote_head_name), + git_str_cstr(&remote_branch_name), + true, + reflog_message); + +cleanup: + git_reference_free(remote_head); + git_str_dispose(&remote_branch_name); + git_str_dispose(&remote_head_name); + return error; +} + +static int update_head_to_remote( + git_repository *repo, + git_remote *remote, + const char *reflog_message) +{ + int error = 0; + size_t refs_len; + const git_remote_head *remote_head, **refs; + const git_oid *remote_head_id; + git_str branch = GIT_STR_INIT; + + if ((error = git_remote_ls(&refs, &refs_len, remote)) < 0) + return error; + + /* We cloned an empty repository or one with an unborn HEAD */ + if (refs_len == 0 || strcmp(refs[0]->name, GIT_HEAD_FILE)) + return update_head_to_default(repo); + + /* We know we have HEAD, let's see where it points */ + remote_head = refs[0]; + GIT_ASSERT(remote_head); + + remote_head_id = &remote_head->oid; + + error = git_remote__default_branch(&branch, remote); + if (error == GIT_ENOTFOUND) { + error = git_repository_set_head_detached( + repo, remote_head_id); + goto cleanup; + } + + if ((error = update_remote_head(repo, remote, &branch, reflog_message)) < 0) + goto cleanup; + + error = update_head_to_new_branch( + repo, + remote_head_id, + git_str_cstr(&branch), + reflog_message); + +cleanup: + git_str_dispose(&branch); + + return error; +} + +static int update_head_to_branch( + git_repository *repo, + git_remote *remote, + const char *branch, + const char *reflog_message) +{ + int retcode; + git_str remote_branch_name = GIT_STR_INIT; + git_reference *remote_ref = NULL; + git_str default_branch = GIT_STR_INIT; + + GIT_ASSERT_ARG(remote); + GIT_ASSERT_ARG(branch); + + if ((retcode = git_str_printf(&remote_branch_name, GIT_REFS_REMOTES_DIR "%s/%s", + git_remote_name(remote), branch)) < 0 ) + goto cleanup; + + if ((retcode = git_reference_lookup(&remote_ref, repo, git_str_cstr(&remote_branch_name))) < 0) + goto cleanup; + + if ((retcode = update_head_to_new_branch(repo, git_reference_target(remote_ref), branch, + reflog_message)) < 0) + goto cleanup; + + retcode = git_remote__default_branch(&default_branch, remote); + + if (retcode == GIT_ENOTFOUND) + retcode = 0; + else if (retcode) + goto cleanup; + + if (!git_remote__matching_refspec(remote, git_str_cstr(&default_branch))) + goto cleanup; + + retcode = update_remote_head(repo, remote, &default_branch, reflog_message); + +cleanup: + git_reference_free(remote_ref); + git_str_dispose(&remote_branch_name); + git_str_dispose(&default_branch); + return retcode; +} + +static int default_repository_create(git_repository **out, const char *path, int bare, void *payload) +{ + GIT_UNUSED(payload); + + return git_repository_init(out, path, bare); +} + +static int default_remote_create( + git_remote **out, + git_repository *repo, + const char *name, + const char *url, + void *payload) +{ + GIT_UNUSED(payload); + + return git_remote_create(out, repo, name, url); +} + +/* + * submodules? + */ + +static int create_and_configure_origin( + git_remote **out, + git_repository *repo, + const char *url, + const git_clone_options *options) +{ + int error; + git_remote *origin = NULL; + char buf[GIT_PATH_MAX]; + git_remote_create_cb remote_create = options->remote_cb; + void *payload = options->remote_cb_payload; + + /* If the path exists and is a dir, the url should be the absolute path */ + if (git_fs_path_root(url) < 0 && git_fs_path_exists(url) && git_fs_path_isdir(url)) { + if (p_realpath(url, buf) == NULL) + return -1; + + url = buf; + } + + if (!remote_create) { + remote_create = default_remote_create; + payload = NULL; + } + + if ((error = remote_create(&origin, repo, "origin", url, payload)) < 0) + goto on_error; + + *out = origin; + return 0; + +on_error: + git_remote_free(origin); + return error; +} + +static bool should_checkout( + git_repository *repo, + bool is_bare, + const git_checkout_options *opts) +{ + if (is_bare) + return false; + + if (!opts) + return false; + + if (opts->checkout_strategy == GIT_CHECKOUT_NONE) + return false; + + return !git_repository_head_unborn(repo); +} + +static int checkout_branch(git_repository *repo, git_remote *remote, const git_checkout_options *co_opts, const char *branch, const char *reflog_message) +{ + int error; + + if (branch) + error = update_head_to_branch(repo, remote, branch, reflog_message); + /* Point HEAD to the same ref as the remote's head */ + else + error = update_head_to_remote(repo, remote, reflog_message); + + if (!error && should_checkout(repo, git_repository_is_bare(repo), co_opts)) + error = git_checkout_head(repo, co_opts); + + return error; +} + +static int clone_into( + git_repository *repo, + git_remote *_remote, + const git_fetch_options *opts, + const git_checkout_options *co_opts, + const char *branch) +{ + int error; + git_str reflog_message = GIT_STR_INIT; + git_remote_connect_options connect_opts = GIT_REMOTE_CONNECT_OPTIONS_INIT; + git_fetch_options fetch_opts; + git_remote *remote; + git_oid_t oid_type; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(_remote); + + if (!git_repository_is_empty(repo)) { + git_error_set(GIT_ERROR_INVALID, "the repository is not empty"); + return -1; + } + + if ((error = git_remote_dup(&remote, _remote)) < 0) + return error; + + memcpy(&fetch_opts, opts, sizeof(git_fetch_options)); + fetch_opts.update_fetchhead = 0; + + if (!opts->depth) + fetch_opts.download_tags = GIT_REMOTE_DOWNLOAD_TAGS_ALL; + + if ((error = git_remote_connect_options__from_fetch_opts(&connect_opts, remote, &fetch_opts)) < 0) + goto cleanup; + + git_str_printf(&reflog_message, "clone: from %s", git_remote_url(remote)); + + /* + * Connect to the server so that we can identify the remote + * object format. + */ + + if ((error = git_remote_connect_ext(remote, GIT_DIRECTION_FETCH, + &connect_opts)) < 0) + goto cleanup; + + if ((error = git_remote_oid_type(&oid_type, remote)) < 0 || + (error = git_repository__set_objectformat(repo, oid_type)) < 0) + goto cleanup; + + if ((error = git_remote_fetch(remote, NULL, &fetch_opts, git_str_cstr(&reflog_message))) != 0) + goto cleanup; + + error = checkout_branch(repo, remote, co_opts, branch, git_str_cstr(&reflog_message)); + +cleanup: + git_remote_free(remote); + git_remote_connect_options_dispose(&connect_opts); + git_str_dispose(&reflog_message); + + return error; +} + +int git_clone__should_clone_local(const char *url_or_path, git_clone_local_t local) +{ + git_str fromurl = GIT_STR_INIT; + const char *path = url_or_path; + bool is_url, is_local; + + if (local == GIT_CLONE_NO_LOCAL) + return 0; + + if ((is_url = git_fs_path_is_local_file_url(url_or_path)) != 0) { + if (git_fs_path_fromurl(&fromurl, url_or_path) < 0) { + is_local = -1; + goto done; + } + + path = fromurl.ptr; + } + + is_local = (!is_url || local != GIT_CLONE_LOCAL_AUTO) && + git_fs_path_isdir(path); + +done: + git_str_dispose(&fromurl); + return is_local; +} + +static int git__clone( + git_repository **out, + const char *url, + const char *local_path, + const git_clone_options *_options, + int use_existing) +{ + int error = 0; + git_repository *repo = NULL; + git_remote *origin; + git_clone_options options = GIT_CLONE_OPTIONS_INIT; + uint32_t rmdir_flags = GIT_RMDIR_REMOVE_FILES; + git_repository_create_cb repository_cb; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(url); + GIT_ASSERT_ARG(local_path); + + if (_options) + memcpy(&options, _options, sizeof(git_clone_options)); + + GIT_ERROR_CHECK_VERSION(&options, GIT_CLONE_OPTIONS_VERSION, "git_clone_options"); + + /* Only clone to a new directory or an empty directory */ + if (git_fs_path_exists(local_path) && !use_existing && !git_fs_path_is_empty_dir(local_path)) { + git_error_set(GIT_ERROR_INVALID, + "'%s' exists and is not an empty directory", local_path); + return GIT_EEXISTS; + } + + /* Only remove the root directory on failure if we create it */ + if (git_fs_path_exists(local_path)) + rmdir_flags |= GIT_RMDIR_SKIP_ROOT; + + if (options.repository_cb) + repository_cb = options.repository_cb; + else + repository_cb = default_repository_create; + + if ((error = repository_cb(&repo, local_path, options.bare, options.repository_cb_payload)) < 0) + return error; + + if (!(error = create_and_configure_origin(&origin, repo, url, &options))) { + int clone_local = git_clone__should_clone_local(url, options.local); + int link = options.local != GIT_CLONE_LOCAL_NO_LINKS; + + if (clone_local == 1) + error = clone_local_into( + repo, origin, &options.fetch_opts, &options.checkout_opts, + options.checkout_branch, link); + else if (clone_local == 0) + error = clone_into( + repo, origin, &options.fetch_opts, &options.checkout_opts, + options.checkout_branch); + else + error = -1; + + git_remote_free(origin); + } + + if (error != 0) { + git_error_state last_error = {0}; + git_error_state_capture(&last_error, error); + + git_repository_free(repo); + repo = NULL; + + (void)git_futils_rmdir_r(local_path, NULL, rmdir_flags); + + git_error_state_restore(&last_error); + } + + *out = repo; + return error; +} + +int git_clone( + git_repository **out, + const char *url, + const char *local_path, + const git_clone_options *_options) +{ + return git__clone(out, url, local_path, _options, 0); +} + +int git_clone__submodule( + git_repository **out, + const char *url, + const char *local_path, + const git_clone_options *_options) +{ + return git__clone(out, url, local_path, _options, 1); +} + +int git_clone_options_init(git_clone_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_clone_options, GIT_CLONE_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_clone_init_options(git_clone_options *opts, unsigned int version) +{ + return git_clone_options_init(opts, version); +} +#endif + +static bool can_link(const char *src, const char *dst, int link) +{ +#ifdef GIT_WIN32 + GIT_UNUSED(src); + GIT_UNUSED(dst); + GIT_UNUSED(link); + return false; +#else + + struct stat st_src, st_dst; + + if (!link) + return false; + + if (p_stat(src, &st_src) < 0) + return false; + + if (p_stat(dst, &st_dst) < 0) + return false; + + return st_src.st_dev == st_dst.st_dev; +#endif +} + +static int clone_local_into(git_repository *repo, git_remote *remote, const git_fetch_options *fetch_opts, const git_checkout_options *co_opts, const char *branch, int link) +{ + int error, flags; + git_repository *src; + git_str src_odb = GIT_STR_INIT, dst_odb = GIT_STR_INIT, src_path = GIT_STR_INIT; + git_str reflog_message = GIT_STR_INIT; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(remote); + + if (!git_repository_is_empty(repo)) { + git_error_set(GIT_ERROR_INVALID, "the repository is not empty"); + return -1; + } + + /* + * Let's figure out what path we should use for the source + * repo, if it's not rooted, the path should be relative to + * the repository's worktree/gitdir. + */ + if ((error = git_fs_path_from_url_or_path(&src_path, git_remote_url(remote))) < 0) + return error; + + /* Copy .git/objects/ from the source to the target */ + if ((error = git_repository_open(&src, git_str_cstr(&src_path))) < 0) { + git_str_dispose(&src_path); + return error; + } + + if (git_repository__item_path(&src_odb, src, GIT_REPOSITORY_ITEM_OBJECTS) < 0 || + git_repository__item_path(&dst_odb, repo, GIT_REPOSITORY_ITEM_OBJECTS) < 0) { + error = -1; + goto cleanup; + } + + flags = 0; + if (can_link(git_repository_path(src), git_repository_path(repo), link)) + flags |= GIT_CPDIR_LINK_FILES; + + error = git_futils_cp_r(git_str_cstr(&src_odb), git_str_cstr(&dst_odb), + flags, GIT_OBJECT_DIR_MODE); + + /* + * can_link() doesn't catch all variations, so if we hit an + * error and did want to link, let's try again without trying + * to link. + */ + if (error < 0 && link) { + flags &= ~GIT_CPDIR_LINK_FILES; + error = git_futils_cp_r(git_str_cstr(&src_odb), git_str_cstr(&dst_odb), + flags, GIT_OBJECT_DIR_MODE); + } + + if (error < 0) + goto cleanup; + + git_str_printf(&reflog_message, "clone: from %s", git_remote_url(remote)); + + if ((error = git_remote_fetch(remote, NULL, fetch_opts, git_str_cstr(&reflog_message))) != 0) + goto cleanup; + + error = checkout_branch(repo, remote, co_opts, branch, git_str_cstr(&reflog_message)); + +cleanup: + git_str_dispose(&reflog_message); + git_str_dispose(&src_path); + git_str_dispose(&src_odb); + git_str_dispose(&dst_odb); + git_repository_free(src); + return error; +} diff --git a/src/libgit2/clone.h b/src/libgit2/clone.h new file mode 100644 index 0000000..7d73cab --- /dev/null +++ b/src/libgit2/clone.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_clone_h__ +#define INCLUDE_clone_h__ + +#include "common.h" + +#include "git2/clone.h" + +extern int git_clone__submodule(git_repository **out, + const char *url, const char *local_path, + const git_clone_options *_options); + +extern int git_clone__should_clone_local(const char *url, git_clone_local_t local); + +#endif diff --git a/src/libgit2/commit.c b/src/libgit2/commit.c new file mode 100644 index 0000000..f7be73a --- /dev/null +++ b/src/libgit2/commit.c @@ -0,0 +1,1099 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "commit.h" + +#include "git2/common.h" +#include "git2/object.h" +#include "git2/repository.h" +#include "git2/signature.h" +#include "git2/mailmap.h" +#include "git2/sys/commit.h" + +#include "buf.h" +#include "odb.h" +#include "commit.h" +#include "signature.h" +#include "refs.h" +#include "object.h" +#include "array.h" +#include "oidarray.h" +#include "grafts.h" + +void git_commit__free(void *_commit) +{ + git_commit *commit = _commit; + + git_array_clear(commit->parent_ids); + + git_signature_free(commit->author); + git_signature_free(commit->committer); + + git__free(commit->raw_header); + git__free(commit->raw_message); + git__free(commit->message_encoding); + git__free(commit->summary); + git__free(commit->body); + + git__free(commit); +} + +static int git_commit__create_buffer_internal( + git_str *out, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_oid *tree, + git_array_oid_t *parents) +{ + size_t i = 0; + const git_oid *parent; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(tree); + + if (git_object__write_oid_header(out, "tree ", tree) < 0) + goto on_error; + + for (i = 0; i < git_array_size(*parents); i++) { + parent = git_array_get(*parents, i); + if (git_object__write_oid_header(out, "parent ", parent) < 0) + goto on_error; + } + + git_signature__writebuf(out, "author ", author); + git_signature__writebuf(out, "committer ", committer); + + if (message_encoding != NULL) + git_str_printf(out, "encoding %s\n", message_encoding); + + git_str_putc(out, '\n'); + + if (git_str_puts(out, message) < 0) + goto on_error; + + return 0; + +on_error: + git_str_dispose(out); + return -1; +} + +static int validate_tree_and_parents(git_array_oid_t *parents, git_repository *repo, const git_oid *tree, + git_commit_parent_callback parent_cb, void *parent_payload, + const git_oid *current_id, bool validate) +{ + size_t i; + int error; + git_oid *parent_cpy; + const git_oid *parent; + + if (validate && !git_object__is_valid(repo, tree, GIT_OBJECT_TREE)) + return -1; + + i = 0; + while ((parent = parent_cb(i, parent_payload)) != NULL) { + if (validate && !git_object__is_valid(repo, parent, GIT_OBJECT_COMMIT)) { + error = -1; + goto on_error; + } + + parent_cpy = git_array_alloc(*parents); + GIT_ERROR_CHECK_ALLOC(parent_cpy); + + git_oid_cpy(parent_cpy, parent); + i++; + } + + if (current_id && (parents->size == 0 || git_oid_cmp(current_id, git_array_get(*parents, 0)))) { + git_error_set(GIT_ERROR_OBJECT, "failed to create commit: current tip is not the first parent"); + error = GIT_EMODIFIED; + goto on_error; + } + + return 0; + +on_error: + git_array_clear(*parents); + return error; +} + +static int git_commit__create_internal( + git_oid *id, + git_repository *repo, + const char *update_ref, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_oid *tree, + git_commit_parent_callback parent_cb, + void *parent_payload, + bool validate) +{ + int error; + git_odb *odb; + git_reference *ref = NULL; + git_str buf = GIT_STR_INIT; + const git_oid *current_id = NULL; + git_array_oid_t parents = GIT_ARRAY_INIT; + + if (update_ref) { + error = git_reference_lookup_resolved(&ref, repo, update_ref, 10); + if (error < 0 && error != GIT_ENOTFOUND) + return error; + } + git_error_clear(); + + if (ref) + current_id = git_reference_target(ref); + + if ((error = validate_tree_and_parents(&parents, repo, tree, parent_cb, parent_payload, current_id, validate)) < 0) + goto cleanup; + + error = git_commit__create_buffer_internal(&buf, author, committer, + message_encoding, message, tree, + &parents); + + if (error < 0) + goto cleanup; + + if (git_repository_odb__weakptr(&odb, repo) < 0) + goto cleanup; + + if (git_odb__freshen(odb, tree) < 0) + goto cleanup; + + if (git_odb_write(id, odb, buf.ptr, buf.size, GIT_OBJECT_COMMIT) < 0) + goto cleanup; + + + if (update_ref != NULL) { + error = git_reference__update_for_commit( + repo, ref, update_ref, id, "commit"); + goto cleanup; + } + +cleanup: + git_array_clear(parents); + git_reference_free(ref); + git_str_dispose(&buf); + return error; +} + +int git_commit_create_from_callback( + git_oid *id, + git_repository *repo, + const char *update_ref, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_oid *tree, + git_commit_parent_callback parent_cb, + void *parent_payload) +{ + return git_commit__create_internal( + id, repo, update_ref, author, committer, message_encoding, message, + tree, parent_cb, parent_payload, true); +} + +typedef struct { + size_t total; + va_list args; +} commit_parent_varargs; + +static const git_oid *commit_parent_from_varargs(size_t curr, void *payload) +{ + commit_parent_varargs *data = payload; + const git_commit *commit; + if (curr >= data->total) + return NULL; + commit = va_arg(data->args, const git_commit *); + return commit ? git_commit_id(commit) : NULL; +} + +int git_commit_create_v( + git_oid *id, + git_repository *repo, + const char *update_ref, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_tree *tree, + size_t parent_count, + ...) +{ + int error = 0; + commit_parent_varargs data; + + GIT_ASSERT_ARG(tree); + GIT_ASSERT_ARG(git_tree_owner(tree) == repo); + + data.total = parent_count; + va_start(data.args, parent_count); + + error = git_commit__create_internal( + id, repo, update_ref, author, committer, + message_encoding, message, git_tree_id(tree), + commit_parent_from_varargs, &data, false); + + va_end(data.args); + return error; +} + +typedef struct { + size_t total; + const git_oid **parents; +} commit_parent_oids; + +static const git_oid *commit_parent_from_ids(size_t curr, void *payload) +{ + commit_parent_oids *data = payload; + return (curr < data->total) ? data->parents[curr] : NULL; +} + +int git_commit_create_from_ids( + git_oid *id, + git_repository *repo, + const char *update_ref, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_oid *tree, + size_t parent_count, + const git_oid *parents[]) +{ + commit_parent_oids data = { parent_count, parents }; + + return git_commit__create_internal( + id, repo, update_ref, author, committer, + message_encoding, message, tree, + commit_parent_from_ids, &data, true); +} + +typedef struct { + size_t total; + const git_commit **parents; + git_repository *repo; +} commit_parent_data; + +static const git_oid *commit_parent_from_array(size_t curr, void *payload) +{ + commit_parent_data *data = payload; + const git_commit *commit; + if (curr >= data->total) + return NULL; + commit = data->parents[curr]; + if (git_commit_owner(commit) != data->repo) + return NULL; + return git_commit_id(commit); +} + +int git_commit_create( + git_oid *id, + git_repository *repo, + const char *update_ref, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_tree *tree, + size_t parent_count, + const git_commit *parents[]) +{ + commit_parent_data data = { parent_count, parents, repo }; + + GIT_ASSERT_ARG(tree); + GIT_ASSERT_ARG(git_tree_owner(tree) == repo); + + return git_commit__create_internal( + id, repo, update_ref, author, committer, + message_encoding, message, git_tree_id(tree), + commit_parent_from_array, &data, false); +} + +static const git_oid *commit_parent_for_amend(size_t curr, void *payload) +{ + const git_commit *commit_to_amend = payload; + if (curr >= git_array_size(commit_to_amend->parent_ids)) + return NULL; + return git_array_get(commit_to_amend->parent_ids, curr); +} + +int git_commit_amend( + git_oid *id, + const git_commit *commit_to_amend, + const char *update_ref, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_tree *tree) +{ + git_repository *repo; + git_oid tree_id; + git_reference *ref; + int error; + + GIT_ASSERT_ARG(id); + GIT_ASSERT_ARG(commit_to_amend); + + repo = git_commit_owner(commit_to_amend); + + if (!author) + author = git_commit_author(commit_to_amend); + if (!committer) + committer = git_commit_committer(commit_to_amend); + if (!message_encoding) + message_encoding = git_commit_message_encoding(commit_to_amend); + if (!message) + message = git_commit_message(commit_to_amend); + + if (!tree) { + git_tree *old_tree; + GIT_ERROR_CHECK_ERROR( git_commit_tree(&old_tree, commit_to_amend) ); + git_oid_cpy(&tree_id, git_tree_id(old_tree)); + git_tree_free(old_tree); + } else { + GIT_ASSERT_ARG(git_tree_owner(tree) == repo); + git_oid_cpy(&tree_id, git_tree_id(tree)); + } + + if (update_ref) { + if ((error = git_reference_lookup_resolved(&ref, repo, update_ref, 5)) < 0) + return error; + + if (git_oid_cmp(git_commit_id(commit_to_amend), git_reference_target(ref))) { + git_reference_free(ref); + git_error_set(GIT_ERROR_REFERENCE, "commit to amend is not the tip of the given branch"); + return -1; + } + } + + error = git_commit__create_internal( + id, repo, NULL, author, committer, message_encoding, message, + &tree_id, commit_parent_for_amend, (void *)commit_to_amend, false); + + if (!error && update_ref) { + error = git_reference__update_for_commit( + repo, ref, NULL, id, "commit"); + git_reference_free(ref); + } + + return error; +} + +static int commit_parse( + git_commit *commit, + const char *data, + size_t size, + git_commit__parse_options *opts) +{ + const char *buffer_start = data, *buffer; + const char *buffer_end = buffer_start + size; + git_oid parent_id; + size_t header_len; + git_signature dummy_sig; + int error; + + GIT_ASSERT_ARG(commit); + GIT_ASSERT_ARG(data); + GIT_ASSERT_ARG(opts); + + buffer = buffer_start; + + /* Allocate for one, which will allow not to realloc 90% of the time */ + git_array_init_to_size(commit->parent_ids, 1); + GIT_ERROR_CHECK_ARRAY(commit->parent_ids); + + /* The tree is always the first field */ + if (!(opts->flags & GIT_COMMIT_PARSE_QUICK)) { + if (git_object__parse_oid_header(&commit->tree_id, + &buffer, buffer_end, "tree ", + opts->oid_type) < 0) + goto bad_buffer; + } else { + size_t tree_len = strlen("tree ") + git_oid_hexsize(opts->oid_type) + 1; + + if (buffer + tree_len > buffer_end) + goto bad_buffer; + buffer += tree_len; + } + + while (git_object__parse_oid_header(&parent_id, + &buffer, buffer_end, "parent ", + opts->oid_type) == 0) { + git_oid *new_id = git_array_alloc(commit->parent_ids); + GIT_ERROR_CHECK_ALLOC(new_id); + + git_oid_cpy(new_id, &parent_id); + } + + if (!opts || !(opts->flags & GIT_COMMIT_PARSE_QUICK)) { + commit->author = git__malloc(sizeof(git_signature)); + GIT_ERROR_CHECK_ALLOC(commit->author); + + if ((error = git_signature__parse(commit->author, &buffer, buffer_end, "author ", '\n')) < 0) + return error; + } + + /* Some tools create multiple author fields, ignore the extra ones */ + while (!git__prefixncmp(buffer, buffer_end - buffer, "author ")) { + if ((error = git_signature__parse(&dummy_sig, &buffer, buffer_end, "author ", '\n')) < 0) + return error; + + git__free(dummy_sig.name); + git__free(dummy_sig.email); + } + + /* Always parse the committer; we need the commit time */ + commit->committer = git__malloc(sizeof(git_signature)); + GIT_ERROR_CHECK_ALLOC(commit->committer); + + if ((error = git_signature__parse(commit->committer, &buffer, buffer_end, "committer ", '\n')) < 0) + return error; + + if (opts && opts->flags & GIT_COMMIT_PARSE_QUICK) + return 0; + + /* Parse add'l header entries */ + while (buffer < buffer_end) { + const char *eoln = buffer; + if (buffer[-1] == '\n' && buffer[0] == '\n') + break; + + while (eoln < buffer_end && *eoln != '\n') + ++eoln; + + if (git__prefixncmp(buffer, buffer_end - buffer, "encoding ") == 0) { + buffer += strlen("encoding "); + + commit->message_encoding = git__strndup(buffer, eoln - buffer); + GIT_ERROR_CHECK_ALLOC(commit->message_encoding); + } + + if (eoln < buffer_end && *eoln == '\n') + ++eoln; + buffer = eoln; + } + + header_len = buffer - buffer_start; + commit->raw_header = git__strndup(buffer_start, header_len); + GIT_ERROR_CHECK_ALLOC(commit->raw_header); + + /* point "buffer" to data after header, +1 for the final LF */ + buffer = buffer_start + header_len + 1; + + /* extract commit message */ + if (buffer <= buffer_end) + commit->raw_message = git__strndup(buffer, buffer_end - buffer); + else + commit->raw_message = git__strdup(""); + GIT_ERROR_CHECK_ALLOC(commit->raw_message); + + return 0; + +bad_buffer: + git_error_set(GIT_ERROR_OBJECT, "failed to parse bad commit object"); + return GIT_EINVALID; +} + +int git_commit__parse( + void *commit, + git_odb_object *odb_obj, + git_oid_t oid_type) +{ + git_commit__parse_options parse_options = {0}; + parse_options.oid_type = oid_type; + + return git_commit__parse_ext(commit, odb_obj, &parse_options); +} + +int git_commit__parse_raw( + void *commit, + const char *data, + size_t size, + git_oid_t oid_type) +{ + git_commit__parse_options parse_options = {0}; + parse_options.oid_type = oid_type; + + return commit_parse(commit, data, size, &parse_options); +} + +static int assign_commit_parents_from_graft(git_commit *commit, git_commit_graft *graft) { + size_t idx; + git_oid *oid; + + git_array_clear(commit->parent_ids); + git_array_init_to_size(commit->parent_ids, git_array_size(graft->parents)); + git_array_foreach(graft->parents, idx, oid) { + git_oid *id = git_array_alloc(commit->parent_ids); + GIT_ERROR_CHECK_ALLOC(id); + + git_oid_cpy(id, oid); + } + + return 0; +} + +int git_commit__parse_ext( + git_commit *commit, + git_odb_object *odb_obj, + git_commit__parse_options *parse_opts) +{ + git_repository *repo = git_object_owner((git_object *)commit); + git_commit_graft *graft; + int error; + + if ((error = commit_parse(commit, git_odb_object_data(odb_obj), + git_odb_object_size(odb_obj), parse_opts)) < 0) + return error; + + /* Perform necessary grafts */ + if (git_grafts_get(&graft, repo->grafts, git_odb_object_id(odb_obj)) != 0 && + git_grafts_get(&graft, repo->shallow_grafts, git_odb_object_id(odb_obj)) != 0) + return 0; + + return assign_commit_parents_from_graft(commit, graft); +} + +#define GIT_COMMIT_GETTER(_rvalue, _name, _return, _invalid) \ + _rvalue git_commit_##_name(const git_commit *commit) \ + {\ + GIT_ASSERT_ARG_WITH_RETVAL(commit, _invalid); \ + return _return; \ + } + +GIT_COMMIT_GETTER(const git_signature *, author, commit->author, NULL) +GIT_COMMIT_GETTER(const git_signature *, committer, commit->committer, NULL) +GIT_COMMIT_GETTER(const char *, message_raw, commit->raw_message, NULL) +GIT_COMMIT_GETTER(const char *, message_encoding, commit->message_encoding, NULL) +GIT_COMMIT_GETTER(const char *, raw_header, commit->raw_header, NULL) +GIT_COMMIT_GETTER(git_time_t, time, commit->committer->when.time, INT64_MIN) +GIT_COMMIT_GETTER(int, time_offset, commit->committer->when.offset, -1) +GIT_COMMIT_GETTER(unsigned int, parentcount, (unsigned int)git_array_size(commit->parent_ids), 0) +GIT_COMMIT_GETTER(const git_oid *, tree_id, &commit->tree_id, NULL) + +const char *git_commit_message(const git_commit *commit) +{ + const char *message; + + GIT_ASSERT_ARG_WITH_RETVAL(commit, NULL); + + message = commit->raw_message; + + /* trim leading newlines from raw message */ + while (*message && *message == '\n') + ++message; + + return message; +} + +const char *git_commit_summary(git_commit *commit) +{ + git_str summary = GIT_STR_INIT; + const char *msg, *space, *next; + bool space_contains_newline = false; + + GIT_ASSERT_ARG_WITH_RETVAL(commit, NULL); + + if (!commit->summary) { + for (msg = git_commit_message(commit), space = NULL; *msg; ++msg) { + char next_character = msg[0]; + /* stop processing at the end of the first paragraph */ + if (next_character == '\n') { + if (!msg[1]) + break; + if (msg[1] == '\n') + break; + /* stop processing if next line contains only whitespace */ + next = msg + 1; + while (*next && git__isspace_nonlf(*next)) { + ++next; + } + if (!*next || *next == '\n') + break; + } + /* record the beginning of contiguous whitespace runs */ + if (git__isspace(next_character)) { + if(space == NULL) { + space = msg; + space_contains_newline = false; + } + space_contains_newline |= next_character == '\n'; + } + /* the next character is non-space */ + else { + /* process any recorded whitespace */ + if (space) { + if(space_contains_newline) + git_str_putc(&summary, ' '); /* if the space contains a newline, collapse to ' ' */ + else + git_str_put(&summary, space, (msg - space)); /* otherwise copy it */ + space = NULL; + } + /* copy the next character */ + git_str_putc(&summary, next_character); + } + } + + commit->summary = git_str_detach(&summary); + if (!commit->summary) + commit->summary = git__strdup(""); + } + + return commit->summary; +} + +const char *git_commit_body(git_commit *commit) +{ + const char *msg, *end; + + GIT_ASSERT_ARG_WITH_RETVAL(commit, NULL); + + if (!commit->body) { + /* search for end of summary */ + for (msg = git_commit_message(commit); *msg; ++msg) + if (msg[0] == '\n' && (!msg[1] || msg[1] == '\n')) + break; + + /* trim leading and trailing whitespace */ + for (; *msg; ++msg) + if (!git__isspace(*msg)) + break; + for (end = msg + strlen(msg) - 1; msg <= end; --end) + if (!git__isspace(*end)) + break; + + if (*msg) + commit->body = git__strndup(msg, end - msg + 1); + } + + return commit->body; +} + +int git_commit_tree(git_tree **tree_out, const git_commit *commit) +{ + GIT_ASSERT_ARG(commit); + return git_tree_lookup(tree_out, commit->object.repo, &commit->tree_id); +} + +const git_oid *git_commit_parent_id( + const git_commit *commit, unsigned int n) +{ + GIT_ASSERT_ARG_WITH_RETVAL(commit, NULL); + + return git_array_get(commit->parent_ids, n); +} + +int git_commit_parent( + git_commit **parent, const git_commit *commit, unsigned int n) +{ + const git_oid *parent_id; + GIT_ASSERT_ARG(commit); + + parent_id = git_commit_parent_id(commit, n); + if (parent_id == NULL) { + git_error_set(GIT_ERROR_INVALID, "parent %u does not exist", n); + return GIT_ENOTFOUND; + } + + return git_commit_lookup(parent, commit->object.repo, parent_id); +} + +int git_commit_nth_gen_ancestor( + git_commit **ancestor, + const git_commit *commit, + unsigned int n) +{ + git_commit *current, *parent = NULL; + int error; + + GIT_ASSERT_ARG(ancestor); + GIT_ASSERT_ARG(commit); + + if (git_commit_dup(¤t, (git_commit *)commit) < 0) + return -1; + + if (n == 0) { + *ancestor = current; + return 0; + } + + while (n--) { + error = git_commit_parent(&parent, current, 0); + + git_commit_free(current); + + if (error < 0) + return error; + + current = parent; + } + + *ancestor = parent; + return 0; +} + +int git_commit_header_field( + git_buf *out, + const git_commit *commit, + const char *field) +{ + GIT_BUF_WRAP_PRIVATE(out, git_commit__header_field, commit, field); +} + +int git_commit__header_field( + git_str *out, + const git_commit *commit, + const char *field) +{ + const char *eol, *buf = commit->raw_header; + + git_str_clear(out); + + while ((eol = strchr(buf, '\n'))) { + /* We can skip continuations here */ + if (buf[0] == ' ') { + buf = eol + 1; + continue; + } + + /* Skip until we find the field we're after */ + if (git__prefixcmp(buf, field)) { + buf = eol + 1; + continue; + } + + buf += strlen(field); + /* Check that we're not matching a prefix but the field itself */ + if (buf[0] != ' ') { + buf = eol + 1; + continue; + } + + buf++; /* skip the SP */ + + git_str_put(out, buf, eol - buf); + if (git_str_oom(out)) + goto oom; + + /* If the next line starts with SP, it's multi-line, we must continue */ + while (eol[1] == ' ') { + git_str_putc(out, '\n'); + buf = eol + 2; + eol = strchr(buf, '\n'); + if (!eol) + goto malformed; + + git_str_put(out, buf, eol - buf); + } + + if (git_str_oom(out)) + goto oom; + + return 0; + } + + git_error_set(GIT_ERROR_OBJECT, "no such field '%s'", field); + return GIT_ENOTFOUND; + +malformed: + git_error_set(GIT_ERROR_OBJECT, "malformed header"); + return -1; +oom: + git_error_set_oom(); + return -1; +} + +int git_commit_extract_signature( + git_buf *signature_out, + git_buf *signed_data_out, + git_repository *repo, + git_oid *commit_id, + const char *field) +{ + git_str signature = GIT_STR_INIT, signed_data = GIT_STR_INIT; + int error; + + if ((error = git_buf_tostr(&signature, signature_out)) < 0 || + (error = git_buf_tostr(&signed_data, signed_data_out)) < 0 || + (error = git_commit__extract_signature(&signature, &signed_data, repo, commit_id, field)) < 0 || + (error = git_buf_fromstr(signature_out, &signature)) < 0 || + (error = git_buf_fromstr(signed_data_out, &signed_data)) < 0) + goto done; + +done: + git_str_dispose(&signature); + git_str_dispose(&signed_data); + return error; +} + +int git_commit__extract_signature( + git_str *signature, + git_str *signed_data, + git_repository *repo, + git_oid *commit_id, + const char *field) +{ + git_odb_object *obj; + git_odb *odb; + const char *buf; + const char *h, *eol; + int error; + + git_str_clear(signature); + git_str_clear(signed_data); + + if (!field) + field = "gpgsig"; + + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0) + return error; + + if ((error = git_odb_read(&obj, odb, commit_id)) < 0) + return error; + + if (obj->cached.type != GIT_OBJECT_COMMIT) { + git_error_set(GIT_ERROR_INVALID, "the requested type does not match the type in the ODB"); + error = GIT_ENOTFOUND; + goto cleanup; + } + + buf = git_odb_object_data(obj); + + while ((h = strchr(buf, '\n')) && h[1] != '\0') { + h++; + if (git__prefixcmp(buf, field)) { + if (git_str_put(signed_data, buf, h - buf) < 0) + return -1; + + buf = h; + continue; + } + + h = buf; + h += strlen(field); + eol = strchr(h, '\n'); + if (h[0] != ' ') { + buf = h; + continue; + } + if (!eol) + goto malformed; + + h++; /* skip the SP */ + + git_str_put(signature, h, eol - h); + if (git_str_oom(signature)) + goto oom; + + /* If the next line starts with SP, it's multi-line, we must continue */ + while (eol[1] == ' ') { + git_str_putc(signature, '\n'); + h = eol + 2; + eol = strchr(h, '\n'); + if (!eol) + goto malformed; + + git_str_put(signature, h, eol - h); + } + + if (git_str_oom(signature)) + goto oom; + + error = git_str_puts(signed_data, eol+1); + git_odb_object_free(obj); + return error; + } + + git_error_set(GIT_ERROR_OBJECT, "this commit is not signed"); + error = GIT_ENOTFOUND; + goto cleanup; + +malformed: + git_error_set(GIT_ERROR_OBJECT, "malformed header"); + error = -1; + goto cleanup; +oom: + git_error_set_oom(); + error = -1; + goto cleanup; + +cleanup: + git_odb_object_free(obj); + git_str_clear(signature); + git_str_clear(signed_data); + return error; +} + +int git_commit_create_buffer( + git_buf *out, + git_repository *repo, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_tree *tree, + size_t parent_count, + const git_commit *parents[]) +{ + GIT_BUF_WRAP_PRIVATE(out, git_commit__create_buffer, repo, + author, committer, message_encoding, message, + tree, parent_count, parents); +} + +int git_commit__create_buffer( + git_str *out, + git_repository *repo, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_tree *tree, + size_t parent_count, + const git_commit *parents[]) +{ + int error; + commit_parent_data data = { parent_count, parents, repo }; + git_array_oid_t parents_arr = GIT_ARRAY_INIT; + const git_oid *tree_id; + + GIT_ASSERT_ARG(tree); + GIT_ASSERT_ARG(git_tree_owner(tree) == repo); + + tree_id = git_tree_id(tree); + + if ((error = validate_tree_and_parents(&parents_arr, repo, tree_id, commit_parent_from_array, &data, NULL, true)) < 0) + return error; + + error = git_commit__create_buffer_internal( + out, author, committer, + message_encoding, message, tree_id, + &parents_arr); + + git_array_clear(parents_arr); + return error; +} + +/** + * Append to 'out' properly marking continuations when there's a newline in 'content' + */ +static int format_header_field(git_str *out, const char *field, const char *content) +{ + const char *lf; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(field); + GIT_ASSERT_ARG(content); + + git_str_puts(out, field); + git_str_putc(out, ' '); + + while ((lf = strchr(content, '\n')) != NULL) { + git_str_put(out, content, lf - content); + git_str_puts(out, "\n "); + content = lf + 1; + } + + git_str_puts(out, content); + git_str_putc(out, '\n'); + + return git_str_oom(out) ? -1 : 0; +} + +static const git_oid *commit_parent_from_commit(size_t n, void *payload) +{ + const git_commit *commit = (const git_commit *) payload; + + return git_array_get(commit->parent_ids, n); + +} + +int git_commit_create_with_signature( + git_oid *out, + git_repository *repo, + const char *commit_content, + const char *signature, + const char *signature_field) +{ + git_odb *odb; + int error = 0; + const char *field; + const char *header_end; + git_str commit = GIT_STR_INIT; + git_commit *parsed; + git_array_oid_t parents = GIT_ARRAY_INIT; + git_commit__parse_options parse_opts = {0}; + + parse_opts.oid_type = repo->oid_type; + + /* The first step is to verify that all the tree and parents exist */ + parsed = git__calloc(1, sizeof(git_commit)); + GIT_ERROR_CHECK_ALLOC(parsed); + if (commit_parse(parsed, commit_content, strlen(commit_content), &parse_opts) < 0) { + error = -1; + goto cleanup; + } + + if ((error = validate_tree_and_parents(&parents, repo, &parsed->tree_id, commit_parent_from_commit, parsed, NULL, true)) < 0) + goto cleanup; + + git_array_clear(parents); + + /* Then we start appending by identifying the end of the commit header */ + header_end = strstr(commit_content, "\n\n"); + if (!header_end) { + git_error_set(GIT_ERROR_INVALID, "malformed commit contents"); + error = -1; + goto cleanup; + } + + /* The header ends after the first LF */ + header_end++; + git_str_put(&commit, commit_content, header_end - commit_content); + + if (signature != NULL) { + field = signature_field ? signature_field : "gpgsig"; + + if ((error = format_header_field(&commit, field, signature)) < 0) + goto cleanup; + } + + git_str_puts(&commit, header_end); + + if (git_str_oom(&commit)) + return -1; + + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0) + goto cleanup; + + if ((error = git_odb_write(out, odb, commit.ptr, commit.size, GIT_OBJECT_COMMIT)) < 0) + goto cleanup; + +cleanup: + git_commit__free(parsed); + git_str_dispose(&commit); + return error; +} + +int git_commit_committer_with_mailmap( + git_signature **out, const git_commit *commit, const git_mailmap *mailmap) +{ + return git_mailmap_resolve_signature(out, mailmap, commit->committer); +} + +int git_commit_author_with_mailmap( + git_signature **out, const git_commit *commit, const git_mailmap *mailmap) +{ + return git_mailmap_resolve_signature(out, mailmap, commit->author); +} diff --git a/src/libgit2/commit.h b/src/libgit2/commit.h new file mode 100644 index 0000000..c25fee3 --- /dev/null +++ b/src/libgit2/commit.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_commit_h__ +#define INCLUDE_commit_h__ + +#include "common.h" + +#include "git2/commit.h" +#include "tree.h" +#include "repository.h" +#include "array.h" + +#include + +struct git_commit { + git_object object; + + git_array_t(git_oid) parent_ids; + git_oid tree_id; + + git_signature *author; + git_signature *committer; + + char *message_encoding; + char *raw_message; + char *raw_header; + + char *summary; + char *body; +}; + +typedef struct { + git_oid_t oid_type; + unsigned int flags; +} git_commit__parse_options; + +typedef enum { + /** Only parse parents and committer info */ + GIT_COMMIT_PARSE_QUICK = (1 << 0) +} git_commit__parse_flags; + +int git_commit__header_field( + git_str *out, + const git_commit *commit, + const char *field); + +int git_commit__extract_signature( + git_str *signature, + git_str *signed_data, + git_repository *repo, + git_oid *commit_id, + const char *field); + +int git_commit__create_buffer( + git_str *out, + git_repository *repo, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_tree *tree, + size_t parent_count, + const git_commit *parents[]); + +int git_commit__parse( + void *commit, + git_odb_object *obj, + git_oid_t oid_type); + +int git_commit__parse_raw( + void *commit, + const char *data, + size_t size, + git_oid_t oid_type); + +int git_commit__parse_ext( + git_commit *commit, + git_odb_object *odb_obj, + git_commit__parse_options *parse_opts); + +void git_commit__free(void *commit); + +#endif diff --git a/src/libgit2/commit_graph.c b/src/libgit2/commit_graph.c new file mode 100644 index 0000000..4edd711 --- /dev/null +++ b/src/libgit2/commit_graph.c @@ -0,0 +1,1309 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "commit_graph.h" + +#include "array.h" +#include "buf.h" +#include "filebuf.h" +#include "futils.h" +#include "hash.h" +#include "oidarray.h" +#include "oidmap.h" +#include "pack.h" +#include "repository.h" +#include "revwalk.h" + +#define GIT_COMMIT_GRAPH_MISSING_PARENT 0x70000000 +#define GIT_COMMIT_GRAPH_GENERATION_NUMBER_MAX 0x3FFFFFFF +#define GIT_COMMIT_GRAPH_GENERATION_NUMBER_INFINITY 0xFFFFFFFF + +#define COMMIT_GRAPH_SIGNATURE 0x43475048 /* "CGPH" */ +#define COMMIT_GRAPH_VERSION 1 +#define COMMIT_GRAPH_OBJECT_ID_VERSION 1 +struct git_commit_graph_header { + uint32_t signature; + uint8_t version; + uint8_t object_id_version; + uint8_t chunks; + uint8_t base_graph_files; +}; + +#define COMMIT_GRAPH_OID_FANOUT_ID 0x4f494446 /* "OIDF" */ +#define COMMIT_GRAPH_OID_LOOKUP_ID 0x4f49444c /* "OIDL" */ +#define COMMIT_GRAPH_COMMIT_DATA_ID 0x43444154 /* "CDAT" */ +#define COMMIT_GRAPH_EXTRA_EDGE_LIST_ID 0x45444745 /* "EDGE" */ +#define COMMIT_GRAPH_BLOOM_FILTER_INDEX_ID 0x42494458 /* "BIDX" */ +#define COMMIT_GRAPH_BLOOM_FILTER_DATA_ID 0x42444154 /* "BDAT" */ + +struct git_commit_graph_chunk { + off64_t offset; + size_t length; +}; + +typedef git_array_t(size_t) parent_index_array_t; + +struct packed_commit { + size_t index; + git_oid sha1; + git_oid tree_oid; + uint32_t generation; + git_time_t commit_time; + git_array_oid_t parents; + parent_index_array_t parent_indices; +}; + +static void packed_commit_free(struct packed_commit *p) +{ + if (!p) + return; + + git_array_clear(p->parents); + git_array_clear(p->parent_indices); + git__free(p); +} + +static struct packed_commit *packed_commit_new(git_commit *commit) +{ + unsigned int i, parentcount = git_commit_parentcount(commit); + struct packed_commit *p = git__calloc(1, sizeof(struct packed_commit)); + if (!p) + goto cleanup; + + git_array_init_to_size(p->parents, parentcount); + if (parentcount && !p->parents.ptr) + goto cleanup; + + if (git_oid_cpy(&p->sha1, git_commit_id(commit)) < 0) + goto cleanup; + if (git_oid_cpy(&p->tree_oid, git_commit_tree_id(commit)) < 0) + goto cleanup; + p->commit_time = git_commit_time(commit); + + for (i = 0; i < parentcount; ++i) { + git_oid *parent_id = git_array_alloc(p->parents); + if (!parent_id) + goto cleanup; + if (git_oid_cpy(parent_id, git_commit_parent_id(commit, i)) < 0) + goto cleanup; + } + + return p; + +cleanup: + packed_commit_free(p); + return NULL; +} + +typedef int (*commit_graph_write_cb)(const char *buf, size_t size, void *cb_data); + +static int commit_graph_error(const char *message) +{ + git_error_set(GIT_ERROR_ODB, "invalid commit-graph file - %s", message); + return -1; +} + +static int commit_graph_parse_oid_fanout( + git_commit_graph_file *file, + const unsigned char *data, + struct git_commit_graph_chunk *chunk_oid_fanout) +{ + uint32_t i, nr; + if (chunk_oid_fanout->offset == 0) + return commit_graph_error("missing OID Fanout chunk"); + if (chunk_oid_fanout->length == 0) + return commit_graph_error("empty OID Fanout chunk"); + if (chunk_oid_fanout->length != 256 * 4) + return commit_graph_error("OID Fanout chunk has wrong length"); + + file->oid_fanout = (const uint32_t *)(data + chunk_oid_fanout->offset); + nr = 0; + for (i = 0; i < 256; ++i) { + uint32_t n = ntohl(file->oid_fanout[i]); + if (n < nr) + return commit_graph_error("index is non-monotonic"); + nr = n; + } + file->num_commits = nr; + return 0; +} + +static int commit_graph_parse_oid_lookup( + git_commit_graph_file *file, + const unsigned char *data, + struct git_commit_graph_chunk *chunk_oid_lookup) +{ + uint32_t i; + unsigned char *oid, *prev_oid, zero_oid[GIT_OID_MAX_SIZE] = {0}; + size_t oid_size; + + oid_size = git_oid_size(file->oid_type); + + if (chunk_oid_lookup->offset == 0) + return commit_graph_error("missing OID Lookup chunk"); + if (chunk_oid_lookup->length == 0) + return commit_graph_error("empty OID Lookup chunk"); + if (chunk_oid_lookup->length != file->num_commits * oid_size) + return commit_graph_error("OID Lookup chunk has wrong length"); + + file->oid_lookup = oid = (unsigned char *)(data + chunk_oid_lookup->offset); + prev_oid = zero_oid; + for (i = 0; i < file->num_commits; ++i, oid += oid_size) { + if (git_oid_raw_cmp(prev_oid, oid, oid_size) >= 0) + return commit_graph_error("OID Lookup index is non-monotonic"); + prev_oid = oid; + } + + return 0; +} + +static int commit_graph_parse_commit_data( + git_commit_graph_file *file, + const unsigned char *data, + struct git_commit_graph_chunk *chunk_commit_data) +{ + size_t oid_size = git_oid_size(file->oid_type); + + if (chunk_commit_data->offset == 0) + return commit_graph_error("missing Commit Data chunk"); + if (chunk_commit_data->length == 0) + return commit_graph_error("empty Commit Data chunk"); + if (chunk_commit_data->length != file->num_commits * (oid_size + 16)) + return commit_graph_error("Commit Data chunk has wrong length"); + + file->commit_data = data + chunk_commit_data->offset; + + return 0; +} + +static int commit_graph_parse_extra_edge_list( + git_commit_graph_file *file, + const unsigned char *data, + struct git_commit_graph_chunk *chunk_extra_edge_list) +{ + if (chunk_extra_edge_list->length == 0) + return 0; + if (chunk_extra_edge_list->length % 4 != 0) + return commit_graph_error("malformed Extra Edge List chunk"); + + file->extra_edge_list = data + chunk_extra_edge_list->offset; + file->num_extra_edge_list = chunk_extra_edge_list->length / 4; + + return 0; +} + +int git_commit_graph_file_parse( + git_commit_graph_file *file, + const unsigned char *data, + size_t size) +{ + struct git_commit_graph_header *hdr; + const unsigned char *chunk_hdr; + struct git_commit_graph_chunk *last_chunk; + uint32_t i; + uint64_t last_chunk_offset, chunk_offset, trailer_offset; + size_t checksum_size; + int error; + struct git_commit_graph_chunk chunk_oid_fanout = {0}, chunk_oid_lookup = {0}, + chunk_commit_data = {0}, chunk_extra_edge_list = {0}, + chunk_unsupported = {0}; + + GIT_ASSERT_ARG(file); + + checksum_size = git_oid_size(file->oid_type); + + if (size < sizeof(struct git_commit_graph_header) + checksum_size) + return commit_graph_error("commit-graph is too short"); + + hdr = ((struct git_commit_graph_header *)data); + + if (hdr->signature != htonl(COMMIT_GRAPH_SIGNATURE) || hdr->version != COMMIT_GRAPH_VERSION + || hdr->object_id_version != COMMIT_GRAPH_OBJECT_ID_VERSION) { + return commit_graph_error("unsupported commit-graph version"); + } + if (hdr->chunks == 0) + return commit_graph_error("no chunks in commit-graph"); + + /* + * The very first chunk's offset should be after the header, all the chunk + * headers, and a special zero chunk. + */ + last_chunk_offset = sizeof(struct git_commit_graph_header) + (1 + hdr->chunks) * 12; + trailer_offset = size - checksum_size; + + if (trailer_offset < last_chunk_offset) + return commit_graph_error("wrong commit-graph size"); + memcpy(file->checksum, (data + trailer_offset), checksum_size); + + chunk_hdr = data + sizeof(struct git_commit_graph_header); + last_chunk = NULL; + for (i = 0; i < hdr->chunks; ++i, chunk_hdr += 12) { + chunk_offset = ((uint64_t)ntohl(*((uint32_t *)(chunk_hdr + 4)))) << 32 + | ((uint64_t)ntohl(*((uint32_t *)(chunk_hdr + 8)))); + if (chunk_offset < last_chunk_offset) + return commit_graph_error("chunks are non-monotonic"); + if (chunk_offset >= trailer_offset) + return commit_graph_error("chunks extend beyond the trailer"); + if (last_chunk != NULL) + last_chunk->length = (size_t)(chunk_offset - last_chunk_offset); + last_chunk_offset = chunk_offset; + + switch (ntohl(*((uint32_t *)(chunk_hdr + 0)))) { + case COMMIT_GRAPH_OID_FANOUT_ID: + chunk_oid_fanout.offset = last_chunk_offset; + last_chunk = &chunk_oid_fanout; + break; + + case COMMIT_GRAPH_OID_LOOKUP_ID: + chunk_oid_lookup.offset = last_chunk_offset; + last_chunk = &chunk_oid_lookup; + break; + + case COMMIT_GRAPH_COMMIT_DATA_ID: + chunk_commit_data.offset = last_chunk_offset; + last_chunk = &chunk_commit_data; + break; + + case COMMIT_GRAPH_EXTRA_EDGE_LIST_ID: + chunk_extra_edge_list.offset = last_chunk_offset; + last_chunk = &chunk_extra_edge_list; + break; + + case COMMIT_GRAPH_BLOOM_FILTER_INDEX_ID: + case COMMIT_GRAPH_BLOOM_FILTER_DATA_ID: + chunk_unsupported.offset = last_chunk_offset; + last_chunk = &chunk_unsupported; + break; + + default: + return commit_graph_error("unrecognized chunk ID"); + } + } + last_chunk->length = (size_t)(trailer_offset - last_chunk_offset); + + error = commit_graph_parse_oid_fanout(file, data, &chunk_oid_fanout); + if (error < 0) + return error; + error = commit_graph_parse_oid_lookup(file, data, &chunk_oid_lookup); + if (error < 0) + return error; + error = commit_graph_parse_commit_data(file, data, &chunk_commit_data); + if (error < 0) + return error; + error = commit_graph_parse_extra_edge_list(file, data, &chunk_extra_edge_list); + if (error < 0) + return error; + + return 0; +} + +int git_commit_graph_new( + git_commit_graph **cgraph_out, + const char *objects_dir, + bool open_file, + git_oid_t oid_type) +{ + git_commit_graph *cgraph = NULL; + int error = 0; + + GIT_ASSERT_ARG(cgraph_out); + GIT_ASSERT_ARG(objects_dir); + GIT_ASSERT_ARG(oid_type); + + cgraph = git__calloc(1, sizeof(git_commit_graph)); + GIT_ERROR_CHECK_ALLOC(cgraph); + + cgraph->oid_type = oid_type; + + error = git_str_joinpath(&cgraph->filename, objects_dir, "info/commit-graph"); + if (error < 0) + goto error; + + if (open_file) { + error = git_commit_graph_file_open(&cgraph->file, + git_str_cstr(&cgraph->filename), oid_type); + + if (error < 0) + goto error; + + cgraph->checked = 1; + } + + *cgraph_out = cgraph; + return 0; + +error: + git_commit_graph_free(cgraph); + return error; +} + +int git_commit_graph_validate(git_commit_graph *cgraph) { + unsigned char checksum[GIT_HASH_MAX_SIZE]; + git_hash_algorithm_t checksum_type; + size_t checksum_size, trailer_offset; + + checksum_type = git_oid_algorithm(cgraph->oid_type); + checksum_size = git_hash_size(checksum_type); + trailer_offset = cgraph->file->graph_map.len - checksum_size; + + if (cgraph->file->graph_map.len < checksum_size) + return commit_graph_error("map length too small"); + + if (git_hash_buf(checksum, cgraph->file->graph_map.data, trailer_offset, checksum_type) < 0) + return commit_graph_error("could not calculate signature"); + if (memcmp(checksum, cgraph->file->checksum, checksum_size) != 0) + return commit_graph_error("index signature mismatch"); + + return 0; +} + +int git_commit_graph_open( + git_commit_graph **cgraph_out, + const char *objects_dir +#ifdef GIT_EXPERIMENTAL_SHA256 + , git_oid_t oid_type +#endif + ) +{ +#ifndef GIT_EXPERIMENTAL_SHA256 + git_oid_t oid_type = GIT_OID_SHA1; +#endif + int error; + + error = git_commit_graph_new(cgraph_out, objects_dir, true, + oid_type); + + if (!error) + return git_commit_graph_validate(*cgraph_out); + + return error; +} + +int git_commit_graph_file_open( + git_commit_graph_file **file_out, + const char *path, + git_oid_t oid_type) +{ + git_commit_graph_file *file; + git_file fd = -1; + size_t cgraph_size; + struct stat st; + int error; + + /* TODO: properly open the file without access time using O_NOATIME */ + fd = git_futils_open_ro(path); + if (fd < 0) + return fd; + + if (p_fstat(fd, &st) < 0) { + p_close(fd); + git_error_set(GIT_ERROR_ODB, "commit-graph file not found - '%s'", path); + return GIT_ENOTFOUND; + } + + if (!S_ISREG(st.st_mode) || !git__is_sizet(st.st_size)) { + p_close(fd); + git_error_set(GIT_ERROR_ODB, "invalid pack index '%s'", path); + return GIT_ENOTFOUND; + } + cgraph_size = (size_t)st.st_size; + + file = git__calloc(1, sizeof(git_commit_graph_file)); + GIT_ERROR_CHECK_ALLOC(file); + + file->oid_type = oid_type; + + error = git_futils_mmap_ro(&file->graph_map, fd, 0, cgraph_size); + p_close(fd); + if (error < 0) { + git_commit_graph_file_free(file); + return error; + } + + if ((error = git_commit_graph_file_parse(file, file->graph_map.data, cgraph_size)) < 0) { + git_commit_graph_file_free(file); + return error; + } + + *file_out = file; + return 0; +} + +int git_commit_graph_get_file( + git_commit_graph_file **file_out, + git_commit_graph *cgraph) +{ + if (!cgraph->checked) { + int error = 0; + git_commit_graph_file *result = NULL; + + /* We only check once, no matter the result. */ + cgraph->checked = 1; + + /* Best effort */ + error = git_commit_graph_file_open(&result, + git_str_cstr(&cgraph->filename), cgraph->oid_type); + + if (error < 0) + return error; + + cgraph->file = result; + } + if (!cgraph->file) + return GIT_ENOTFOUND; + + *file_out = cgraph->file; + return 0; +} + +void git_commit_graph_refresh(git_commit_graph *cgraph) +{ + if (!cgraph->checked) + return; + + if (cgraph->file + && git_commit_graph_file_needs_refresh(cgraph->file, git_str_cstr(&cgraph->filename))) { + /* We just free the commit graph. The next time it is requested, it will be + * re-loaded. */ + git_commit_graph_file_free(cgraph->file); + cgraph->file = NULL; + } + /* Force a lazy re-check next time it is needed. */ + cgraph->checked = 0; +} + +static int git_commit_graph_entry_get_byindex( + git_commit_graph_entry *e, + const git_commit_graph_file *file, + size_t pos) +{ + const unsigned char *commit_data; + size_t oid_size = git_oid_size(file->oid_type); + + GIT_ASSERT_ARG(e); + GIT_ASSERT_ARG(file); + + if (pos >= file->num_commits) { + git_error_set(GIT_ERROR_INVALID, "commit index %zu does not exist", pos); + return GIT_ENOTFOUND; + } + + commit_data = file->commit_data + pos * (oid_size + 4 * sizeof(uint32_t)); + git_oid__fromraw(&e->tree_oid, commit_data, file->oid_type); + e->parent_indices[0] = ntohl(*((uint32_t *)(commit_data + oid_size))); + e->parent_indices[1] = ntohl( + *((uint32_t *)(commit_data + oid_size + sizeof(uint32_t)))); + e->parent_count = (e->parent_indices[0] != GIT_COMMIT_GRAPH_MISSING_PARENT) + + (e->parent_indices[1] != GIT_COMMIT_GRAPH_MISSING_PARENT); + e->generation = ntohl(*((uint32_t *)(commit_data + oid_size + 2 * sizeof(uint32_t)))); + e->commit_time = ntohl(*((uint32_t *)(commit_data + oid_size + 3 * sizeof(uint32_t)))); + + e->commit_time |= (e->generation & UINT64_C(0x3)) << UINT64_C(32); + e->generation >>= 2u; + if (e->parent_indices[1] & 0x80000000u) { + uint32_t extra_edge_list_pos = e->parent_indices[1] & 0x7fffffff; + + /* Make sure we're not being sent out of bounds */ + if (extra_edge_list_pos >= file->num_extra_edge_list) { + git_error_set(GIT_ERROR_INVALID, + "commit %u does not exist", + extra_edge_list_pos); + return GIT_ENOTFOUND; + } + + e->extra_parents_index = extra_edge_list_pos; + while (extra_edge_list_pos < file->num_extra_edge_list + && (ntohl(*( + (uint32_t *)(file->extra_edge_list + + extra_edge_list_pos * sizeof(uint32_t)))) + & 0x80000000u) + == 0) { + extra_edge_list_pos++; + e->parent_count++; + } + } + + git_oid__fromraw(&e->sha1, &file->oid_lookup[pos * oid_size], file->oid_type); + return 0; +} + +bool git_commit_graph_file_needs_refresh(const git_commit_graph_file *file, const char *path) +{ + git_file fd = -1; + struct stat st; + ssize_t bytes_read; + unsigned char checksum[GIT_HASH_MAX_SIZE]; + size_t checksum_size = git_oid_size(file->oid_type); + + /* TODO: properly open the file without access time using O_NOATIME */ + fd = git_futils_open_ro(path); + if (fd < 0) + return true; + + if (p_fstat(fd, &st) < 0) { + p_close(fd); + return true; + } + + if (!S_ISREG(st.st_mode) || !git__is_sizet(st.st_size) + || (size_t)st.st_size != file->graph_map.len) { + p_close(fd); + return true; + } + + bytes_read = p_pread(fd, checksum, checksum_size, st.st_size - checksum_size); + p_close(fd); + if (bytes_read != (ssize_t)checksum_size) + return true; + + return (memcmp(checksum, file->checksum, checksum_size) != 0); +} + +int git_commit_graph_entry_find( + git_commit_graph_entry *e, + const git_commit_graph_file *file, + const git_oid *short_oid, + size_t len) +{ + int pos, found = 0; + uint32_t hi, lo; + const unsigned char *current = NULL; + size_t oid_size, oid_hexsize; + + GIT_ASSERT_ARG(e); + GIT_ASSERT_ARG(file); + GIT_ASSERT_ARG(short_oid); + + oid_size = git_oid_size(file->oid_type); + oid_hexsize = git_oid_hexsize(file->oid_type); + + hi = ntohl(file->oid_fanout[(int)short_oid->id[0]]); + lo = ((short_oid->id[0] == 0x0) ? 0 : ntohl(file->oid_fanout[(int)short_oid->id[0] - 1])); + + pos = git_pack__lookup_id(file->oid_lookup, oid_size, lo, hi, + short_oid->id, file->oid_type); + + if (pos >= 0) { + /* An object matching exactly the oid was found */ + found = 1; + current = file->oid_lookup + (pos * oid_size); + } else { + /* No object was found */ + /* pos refers to the object with the "closest" oid to short_oid */ + pos = -1 - pos; + if (pos < (int)file->num_commits) { + current = file->oid_lookup + (pos * oid_size); + + if (!git_oid_raw_ncmp(short_oid->id, current, len)) + found = 1; + } + } + + if (found && len != oid_hexsize && pos + 1 < (int)file->num_commits) { + /* Check for ambiguousity */ + const unsigned char *next = current + oid_size; + + if (!git_oid_raw_ncmp(short_oid->id, next, len)) + found = 2; + } + + if (!found) + return git_odb__error_notfound( + "failed to find offset for commit-graph index entry", short_oid, len); + if (found > 1) + return git_odb__error_ambiguous( + "found multiple offsets for commit-graph index entry"); + + return git_commit_graph_entry_get_byindex(e, file, pos); +} + +int git_commit_graph_entry_parent( + git_commit_graph_entry *parent, + const git_commit_graph_file *file, + const git_commit_graph_entry *entry, + size_t n) +{ + GIT_ASSERT_ARG(parent); + GIT_ASSERT_ARG(file); + + if (n >= entry->parent_count) { + git_error_set(GIT_ERROR_INVALID, "parent index %zu does not exist", n); + return GIT_ENOTFOUND; + } + + if (n == 0 || (n == 1 && entry->parent_count == 2)) + return git_commit_graph_entry_get_byindex(parent, file, entry->parent_indices[n]); + + return git_commit_graph_entry_get_byindex( + parent, + file, + ntohl( + *(uint32_t *)(file->extra_edge_list + + (entry->extra_parents_index + n - 1) + * sizeof(uint32_t))) + & 0x7fffffff); +} + +int git_commit_graph_file_close(git_commit_graph_file *file) +{ + GIT_ASSERT_ARG(file); + + if (file->graph_map.data) + git_futils_mmap_free(&file->graph_map); + + return 0; +} + +void git_commit_graph_free(git_commit_graph *cgraph) +{ + if (!cgraph) + return; + + git_str_dispose(&cgraph->filename); + git_commit_graph_file_free(cgraph->file); + git__free(cgraph); +} + +void git_commit_graph_file_free(git_commit_graph_file *file) +{ + if (!file) + return; + + git_commit_graph_file_close(file); + git__free(file); +} + +static int packed_commit__cmp(const void *a_, const void *b_) +{ + const struct packed_commit *a = a_; + const struct packed_commit *b = b_; + return git_oid_cmp(&a->sha1, &b->sha1); +} + +int git_commit_graph_writer_new( + git_commit_graph_writer **out, + const char *objects_info_dir +#ifdef GIT_EXPERIMENTAL_SHA256 + , git_oid_t oid_type +#endif + ) +{ + git_commit_graph_writer *w; + +#ifndef GIT_EXPERIMENTAL_SHA256 + git_oid_t oid_type = GIT_OID_SHA1; +#endif + + GIT_ASSERT_ARG(out && objects_info_dir && oid_type); + + w = git__calloc(1, sizeof(git_commit_graph_writer)); + GIT_ERROR_CHECK_ALLOC(w); + + w->oid_type = oid_type; + + if (git_str_sets(&w->objects_info_dir, objects_info_dir) < 0) { + git__free(w); + return -1; + } + + if (git_vector_init(&w->commits, 0, packed_commit__cmp) < 0) { + git_str_dispose(&w->objects_info_dir); + git__free(w); + return -1; + } + + *out = w; + return 0; +} + +void git_commit_graph_writer_free(git_commit_graph_writer *w) +{ + struct packed_commit *packed_commit; + size_t i; + + if (!w) + return; + + git_vector_foreach (&w->commits, i, packed_commit) + packed_commit_free(packed_commit); + git_vector_free(&w->commits); + git_str_dispose(&w->objects_info_dir); + git__free(w); +} + +struct object_entry_cb_state { + git_repository *repo; + git_odb *db; + git_vector *commits; +}; + +static int object_entry__cb(const git_oid *id, void *data) +{ + struct object_entry_cb_state *state = (struct object_entry_cb_state *)data; + git_commit *commit = NULL; + struct packed_commit *packed_commit = NULL; + size_t header_len; + git_object_t header_type; + int error = 0; + + error = git_odb_read_header(&header_len, &header_type, state->db, id); + if (error < 0) + return error; + + if (header_type != GIT_OBJECT_COMMIT) + return 0; + + error = git_commit_lookup(&commit, state->repo, id); + if (error < 0) + return error; + + packed_commit = packed_commit_new(commit); + git_commit_free(commit); + GIT_ERROR_CHECK_ALLOC(packed_commit); + + error = git_vector_insert(state->commits, packed_commit); + if (error < 0) { + packed_commit_free(packed_commit); + return error; + } + + return 0; +} + +int git_commit_graph_writer_add_index_file( + git_commit_graph_writer *w, + git_repository *repo, + const char *idx_path) +{ + int error; + struct git_pack_file *p = NULL; + struct object_entry_cb_state state = {0}; + state.repo = repo; + state.commits = &w->commits; + + error = git_repository_odb(&state.db, repo); + if (error < 0) + goto cleanup; + + /* TODO: SHA256 */ + error = git_mwindow_get_pack(&p, idx_path, 0); + if (error < 0) + goto cleanup; + + error = git_pack_foreach_entry(p, object_entry__cb, &state); + if (error < 0) + goto cleanup; + +cleanup: + if (p) + git_mwindow_put_pack(p); + git_odb_free(state.db); + return error; +} + +int git_commit_graph_writer_add_revwalk(git_commit_graph_writer *w, git_revwalk *walk) +{ + int error; + git_oid id; + git_repository *repo = git_revwalk_repository(walk); + git_commit *commit; + struct packed_commit *packed_commit; + + while ((git_revwalk_next(&id, walk)) == 0) { + error = git_commit_lookup(&commit, repo, &id); + if (error < 0) + return error; + + packed_commit = packed_commit_new(commit); + git_commit_free(commit); + GIT_ERROR_CHECK_ALLOC(packed_commit); + + error = git_vector_insert(&w->commits, packed_commit); + if (error < 0) { + packed_commit_free(packed_commit); + return error; + } + } + + return 0; +} + +enum generation_number_commit_state { + GENERATION_NUMBER_COMMIT_STATE_UNVISITED = 0, + GENERATION_NUMBER_COMMIT_STATE_ADDED = 1, + GENERATION_NUMBER_COMMIT_STATE_EXPANDED = 2, + GENERATION_NUMBER_COMMIT_STATE_VISITED = 3 +}; + +static int compute_generation_numbers(git_vector *commits) +{ + git_array_t(size_t) index_stack = GIT_ARRAY_INIT; + size_t i, j; + size_t *parent_idx; + enum generation_number_commit_state *commit_states = NULL; + struct packed_commit *child_packed_commit; + git_oidmap *packed_commit_map = NULL; + int error = 0; + + /* First populate the parent indices fields */ + error = git_oidmap_new(&packed_commit_map); + if (error < 0) + goto cleanup; + git_vector_foreach (commits, i, child_packed_commit) { + child_packed_commit->index = i; + error = git_oidmap_set( + packed_commit_map, &child_packed_commit->sha1, child_packed_commit); + if (error < 0) + goto cleanup; + } + + git_vector_foreach (commits, i, child_packed_commit) { + size_t parent_i, *parent_idx_ptr; + struct packed_commit *parent_packed_commit; + git_oid *parent_id; + git_array_init_to_size( + child_packed_commit->parent_indices, + git_array_size(child_packed_commit->parents)); + if (git_array_size(child_packed_commit->parents) + && !child_packed_commit->parent_indices.ptr) { + error = -1; + goto cleanup; + } + git_array_foreach (child_packed_commit->parents, parent_i, parent_id) { + parent_packed_commit = git_oidmap_get(packed_commit_map, parent_id); + if (!parent_packed_commit) { + git_error_set(GIT_ERROR_ODB, + "parent commit %s not found in commit graph", + git_oid_tostr_s(parent_id)); + error = GIT_ENOTFOUND; + goto cleanup; + } + parent_idx_ptr = git_array_alloc(child_packed_commit->parent_indices); + if (!parent_idx_ptr) { + error = -1; + goto cleanup; + } + *parent_idx_ptr = parent_packed_commit->index; + } + } + + /* + * We copy all the commits to the stack and then during visitation, + * each node can be added up to two times to the stack. + */ + git_array_init_to_size(index_stack, 3 * git_vector_length(commits)); + if (!index_stack.ptr) { + error = -1; + goto cleanup; + } + + commit_states = (enum generation_number_commit_state *)git__calloc( + git_vector_length(commits), sizeof(enum generation_number_commit_state)); + if (!commit_states) { + error = -1; + goto cleanup; + } + + /* + * Perform a Post-Order traversal so that all parent nodes are fully + * visited before the child node. + */ + git_vector_foreach (commits, i, child_packed_commit) + *(size_t *)git_array_alloc(index_stack) = i; + + while (git_array_size(index_stack)) { + size_t *index_ptr = git_array_pop(index_stack); + i = *index_ptr; + child_packed_commit = git_vector_get(commits, i); + + if (commit_states[i] == GENERATION_NUMBER_COMMIT_STATE_VISITED) { + /* This commit has already been fully visited. */ + continue; + } + if (commit_states[i] == GENERATION_NUMBER_COMMIT_STATE_EXPANDED) { + /* All of the commits parents have been visited. */ + child_packed_commit->generation = 0; + git_array_foreach (child_packed_commit->parent_indices, j, parent_idx) { + struct packed_commit *parent = git_vector_get(commits, *parent_idx); + if (child_packed_commit->generation < parent->generation) + child_packed_commit->generation = parent->generation; + } + if (child_packed_commit->generation + < GIT_COMMIT_GRAPH_GENERATION_NUMBER_MAX) { + ++child_packed_commit->generation; + } + commit_states[i] = GENERATION_NUMBER_COMMIT_STATE_VISITED; + continue; + } + + /* + * This is the first time we see this commit. We need + * to visit all its parents before we can fully visit + * it. + */ + if (git_array_size(child_packed_commit->parent_indices) == 0) { + /* + * Special case: if the commit has no parents, there's + * no need to add it to the stack just to immediately + * remove it. + */ + commit_states[i] = GENERATION_NUMBER_COMMIT_STATE_VISITED; + child_packed_commit->generation = 1; + continue; + } + + /* + * Add this current commit again so that it is visited + * again once all its children have been visited. + */ + *(size_t *)git_array_alloc(index_stack) = i; + git_array_foreach (child_packed_commit->parent_indices, j, parent_idx) { + if (commit_states[*parent_idx] + != GENERATION_NUMBER_COMMIT_STATE_UNVISITED) { + /* This commit has already been considered. */ + continue; + } + + commit_states[*parent_idx] = GENERATION_NUMBER_COMMIT_STATE_ADDED; + *(size_t *)git_array_alloc(index_stack) = *parent_idx; + } + commit_states[i] = GENERATION_NUMBER_COMMIT_STATE_EXPANDED; + } + +cleanup: + git_oidmap_free(packed_commit_map); + git__free(commit_states); + git_array_clear(index_stack); + + return error; +} + +static int write_offset(off64_t offset, commit_graph_write_cb write_cb, void *cb_data) +{ + int error; + uint32_t word; + + word = htonl((uint32_t)((offset >> 32) & 0xffffffffu)); + error = write_cb((const char *)&word, sizeof(word), cb_data); + if (error < 0) + return error; + word = htonl((uint32_t)((offset >> 0) & 0xffffffffu)); + error = write_cb((const char *)&word, sizeof(word), cb_data); + if (error < 0) + return error; + + return 0; +} + +static int write_chunk_header( + int chunk_id, + off64_t offset, + commit_graph_write_cb write_cb, + void *cb_data) +{ + uint32_t word = htonl(chunk_id); + int error = write_cb((const char *)&word, sizeof(word), cb_data); + if (error < 0) + return error; + return write_offset(offset, write_cb, cb_data); +} + +static int commit_graph_write_buf(const char *buf, size_t size, void *data) +{ + git_str *b = (git_str *)data; + return git_str_put(b, buf, size); +} + +struct commit_graph_write_hash_context { + commit_graph_write_cb write_cb; + void *cb_data; + git_hash_ctx *ctx; +}; + +static int commit_graph_write_hash(const char *buf, size_t size, void *data) +{ + struct commit_graph_write_hash_context *ctx = data; + int error; + + error = git_hash_update(ctx->ctx, buf, size); + if (error < 0) + return error; + + return ctx->write_cb(buf, size, ctx->cb_data); +} + +static void packed_commit_free_dup(void *packed_commit) +{ + packed_commit_free(packed_commit); +} + +static int commit_graph_write( + git_commit_graph_writer *w, + commit_graph_write_cb write_cb, + void *cb_data) +{ + int error = 0; + size_t i; + struct packed_commit *packed_commit; + struct git_commit_graph_header hdr = {0}; + uint32_t oid_fanout_count; + uint32_t extra_edge_list_count; + uint32_t oid_fanout[256]; + off64_t offset; + git_str oid_lookup = GIT_STR_INIT, commit_data = GIT_STR_INIT, + extra_edge_list = GIT_STR_INIT; + unsigned char checksum[GIT_HASH_MAX_SIZE]; + git_hash_algorithm_t checksum_type; + size_t checksum_size, oid_size; + git_hash_ctx ctx; + struct commit_graph_write_hash_context hash_cb_data = {0}; + + hdr.signature = htonl(COMMIT_GRAPH_SIGNATURE); + hdr.version = COMMIT_GRAPH_VERSION; + hdr.object_id_version = COMMIT_GRAPH_OBJECT_ID_VERSION; + hdr.chunks = 0; + hdr.base_graph_files = 0; + hash_cb_data.write_cb = write_cb; + hash_cb_data.cb_data = cb_data; + hash_cb_data.ctx = &ctx; + + oid_size = git_oid_size(w->oid_type); + checksum_type = git_oid_algorithm(w->oid_type); + checksum_size = git_hash_size(checksum_type); + + error = git_hash_ctx_init(&ctx, checksum_type); + if (error < 0) + return error; + cb_data = &hash_cb_data; + write_cb = commit_graph_write_hash; + + /* Sort the commits. */ + git_vector_sort(&w->commits); + git_vector_uniq(&w->commits, packed_commit_free_dup); + error = compute_generation_numbers(&w->commits); + if (error < 0) + goto cleanup; + + /* Fill the OID Fanout table. */ + oid_fanout_count = 0; + for (i = 0; i < 256; i++) { + while (oid_fanout_count < git_vector_length(&w->commits) && + (packed_commit = (struct packed_commit *)git_vector_get(&w->commits, oid_fanout_count)) && + packed_commit->sha1.id[0] <= i) + ++oid_fanout_count; + oid_fanout[i] = htonl(oid_fanout_count); + } + + /* Fill the OID Lookup table. */ + git_vector_foreach (&w->commits, i, packed_commit) { + error = git_str_put(&oid_lookup, + (const char *)&packed_commit->sha1.id, + oid_size); + + if (error < 0) + goto cleanup; + } + + /* Fill the Commit Data and Extra Edge List tables. */ + extra_edge_list_count = 0; + git_vector_foreach (&w->commits, i, packed_commit) { + uint64_t commit_time; + uint32_t generation; + uint32_t word; + size_t *packed_index; + unsigned int parentcount = (unsigned int)git_array_size(packed_commit->parents); + + error = git_str_put(&commit_data, + (const char *)&packed_commit->tree_oid.id, + oid_size); + + if (error < 0) + goto cleanup; + + if (parentcount == 0) { + word = htonl(GIT_COMMIT_GRAPH_MISSING_PARENT); + } else { + packed_index = git_array_get(packed_commit->parent_indices, 0); + word = htonl((uint32_t)*packed_index); + } + error = git_str_put(&commit_data, (const char *)&word, sizeof(word)); + if (error < 0) + goto cleanup; + + if (parentcount < 2) { + word = htonl(GIT_COMMIT_GRAPH_MISSING_PARENT); + } else if (parentcount == 2) { + packed_index = git_array_get(packed_commit->parent_indices, 1); + word = htonl((uint32_t)*packed_index); + } else { + word = htonl(0x80000000u | extra_edge_list_count); + } + error = git_str_put(&commit_data, (const char *)&word, sizeof(word)); + if (error < 0) + goto cleanup; + + if (parentcount > 2) { + unsigned int parent_i; + for (parent_i = 1; parent_i < parentcount; ++parent_i) { + packed_index = git_array_get( + packed_commit->parent_indices, parent_i); + word = htonl((uint32_t)(*packed_index | (parent_i + 1 == parentcount ? 0x80000000u : 0))); + + error = git_str_put(&extra_edge_list, + (const char *)&word, + sizeof(word)); + if (error < 0) + goto cleanup; + } + extra_edge_list_count += parentcount - 1; + } + + generation = packed_commit->generation; + commit_time = (uint64_t)packed_commit->commit_time; + if (generation > GIT_COMMIT_GRAPH_GENERATION_NUMBER_MAX) + generation = GIT_COMMIT_GRAPH_GENERATION_NUMBER_MAX; + word = ntohl((uint32_t)((generation << 2) | (((uint32_t)(commit_time >> 32)) & 0x3) )); + error = git_str_put(&commit_data, (const char *)&word, sizeof(word)); + if (error < 0) + goto cleanup; + word = ntohl((uint32_t)(commit_time & 0xfffffffful)); + error = git_str_put(&commit_data, (const char *)&word, sizeof(word)); + if (error < 0) + goto cleanup; + } + + /* Write the header. */ + hdr.chunks = 3; + if (git_str_len(&extra_edge_list) > 0) + hdr.chunks++; + error = write_cb((const char *)&hdr, sizeof(hdr), cb_data); + if (error < 0) + goto cleanup; + + /* Write the chunk headers. */ + offset = sizeof(hdr) + (hdr.chunks + 1) * 12; + error = write_chunk_header(COMMIT_GRAPH_OID_FANOUT_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += sizeof(oid_fanout); + error = write_chunk_header(COMMIT_GRAPH_OID_LOOKUP_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += git_str_len(&oid_lookup); + error = write_chunk_header(COMMIT_GRAPH_COMMIT_DATA_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += git_str_len(&commit_data); + if (git_str_len(&extra_edge_list) > 0) { + error = write_chunk_header( + COMMIT_GRAPH_EXTRA_EDGE_LIST_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += git_str_len(&extra_edge_list); + } + error = write_chunk_header(0, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + + /* Write all the chunks. */ + error = write_cb((const char *)oid_fanout, sizeof(oid_fanout), cb_data); + if (error < 0) + goto cleanup; + error = write_cb(git_str_cstr(&oid_lookup), git_str_len(&oid_lookup), cb_data); + if (error < 0) + goto cleanup; + error = write_cb(git_str_cstr(&commit_data), git_str_len(&commit_data), cb_data); + if (error < 0) + goto cleanup; + error = write_cb(git_str_cstr(&extra_edge_list), git_str_len(&extra_edge_list), cb_data); + if (error < 0) + goto cleanup; + + /* Finalize the checksum and write the trailer. */ + error = git_hash_final(checksum, &ctx); + if (error < 0) + goto cleanup; + error = write_cb((char *)checksum, checksum_size, cb_data); + if (error < 0) + goto cleanup; + +cleanup: + git_str_dispose(&oid_lookup); + git_str_dispose(&commit_data); + git_str_dispose(&extra_edge_list); + git_hash_ctx_cleanup(&ctx); + return error; +} + +static int commit_graph_write_filebuf(const char *buf, size_t size, void *data) +{ + git_filebuf *f = (git_filebuf *)data; + return git_filebuf_write(f, buf, size); +} + +int git_commit_graph_writer_options_init( + git_commit_graph_writer_options *opts, + unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, + version, + git_commit_graph_writer_options, + GIT_COMMIT_GRAPH_WRITER_OPTIONS_INIT); + return 0; +} + +int git_commit_graph_writer_commit( + git_commit_graph_writer *w, + git_commit_graph_writer_options *opts) +{ + int error; + int filebuf_flags = GIT_FILEBUF_DO_NOT_BUFFER; + git_str commit_graph_path = GIT_STR_INIT; + git_filebuf output = GIT_FILEBUF_INIT; + + /* TODO: support options and fill in defaults. */ + GIT_UNUSED(opts); + + error = git_str_joinpath( + &commit_graph_path, git_str_cstr(&w->objects_info_dir), "commit-graph"); + if (error < 0) + return error; + + if (git_repository__fsync_gitdir) + filebuf_flags |= GIT_FILEBUF_FSYNC; + error = git_filebuf_open(&output, git_str_cstr(&commit_graph_path), filebuf_flags, 0644); + git_str_dispose(&commit_graph_path); + if (error < 0) + return error; + + error = commit_graph_write(w, commit_graph_write_filebuf, &output); + if (error < 0) { + git_filebuf_cleanup(&output); + return error; + } + + return git_filebuf_commit(&output); +} + +int git_commit_graph_writer_dump( + git_buf *cgraph, + git_commit_graph_writer *w, + git_commit_graph_writer_options *opts) +{ + GIT_BUF_WRAP_PRIVATE(cgraph, git_commit_graph__writer_dump, w, opts); +} + +int git_commit_graph__writer_dump( + git_str *cgraph, + git_commit_graph_writer *w, + git_commit_graph_writer_options *opts) +{ + /* TODO: support options. */ + GIT_UNUSED(opts); + return commit_graph_write(w, commit_graph_write_buf, cgraph); +} diff --git a/src/libgit2/commit_graph.h b/src/libgit2/commit_graph.h new file mode 100644 index 0000000..ecf4379 --- /dev/null +++ b/src/libgit2/commit_graph.h @@ -0,0 +1,188 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_commit_graph_h__ +#define INCLUDE_commit_graph_h__ + +#include "common.h" + +#include "git2/types.h" +#include "git2/sys/commit_graph.h" + +#include "map.h" +#include "vector.h" +#include "oid.h" +#include "hash.h" + +/** + * A commit-graph file. + * + * This file contains metadata about commits, particularly the generation + * number for each one. This can help speed up graph operations without + * requiring a full graph traversal. + * + * Support for this feature was added in git 2.19. + */ +typedef struct git_commit_graph_file { + git_map graph_map; + + /* The type of object IDs in the commit graph file. */ + git_oid_t oid_type; + + /* The OID Fanout table. */ + const uint32_t *oid_fanout; + /* The total number of commits in the graph. */ + uint32_t num_commits; + + /* The OID Lookup table. */ + unsigned char *oid_lookup; + + /* + * The Commit Data table. Each entry contains the OID of the commit followed + * by two 8-byte fields in network byte order: + * - The indices of the first two parents (32 bits each). + * - The generation number (first 30 bits) and commit time in seconds since + * UNIX epoch (34 bits). + */ + const unsigned char *commit_data; + + /* + * The Extra Edge List table. Each 4-byte entry is a network byte order index + * of one of the i-th (i > 0) parents of commits in the `commit_data` table, + * when the commit has more than 2 parents. + */ + const unsigned char *extra_edge_list; + /* The number of entries in the Extra Edge List table. Each entry is 4 bytes wide. */ + size_t num_extra_edge_list; + + /* The trailer of the file. Contains the SHA1-checksum of the whole file. */ + unsigned char checksum[GIT_HASH_SHA1_SIZE]; +} git_commit_graph_file; + +/** + * An entry in the commit-graph file. Provides a subset of the information that + * can be obtained from the commit header. + */ +typedef struct git_commit_graph_entry { + /* The generation number of the commit within the graph */ + size_t generation; + + /* Time in seconds from UNIX epoch. */ + git_time_t commit_time; + + /* The number of parents of the commit. */ + size_t parent_count; + + /* + * The indices of the parent commits within the Commit Data table. The value + * of `GIT_COMMIT_GRAPH_MISSING_PARENT` indicates that no parent is in that + * position. + */ + size_t parent_indices[2]; + + /* The index within the Extra Edge List of any parent after the first two. */ + size_t extra_parents_index; + + /* The object ID of the root tree of the commit. */ + git_oid tree_oid; + + /* The object ID hash of the requested commit. */ + git_oid sha1; +} git_commit_graph_entry; + +/* A wrapper for git_commit_graph_file to enable lazy loading in the ODB. */ +struct git_commit_graph { + /* The path to the commit-graph file. Something like ".git/objects/info/commit-graph". */ + git_str filename; + + /* The underlying commit-graph file. */ + git_commit_graph_file *file; + + /* The object ID types in the commit graph. */ + git_oid_t oid_type; + + /* Whether the commit-graph file was already checked for validity. */ + bool checked; +}; + +/** Create a new commit-graph, optionally opening the underlying file. */ +int git_commit_graph_new( + git_commit_graph **cgraph_out, + const char *objects_dir, + bool open_file, + git_oid_t oid_type); + +/** Validate the checksum of a commit graph */ +int git_commit_graph_validate(git_commit_graph *cgraph); + +/** Open and validate a commit-graph file. */ +int git_commit_graph_file_open( + git_commit_graph_file **file_out, + const char *path, + git_oid_t oid_type); + +/* + * Attempt to get the git_commit_graph's commit-graph file. This object is + * still owned by the git_commit_graph. If the repository does not contain a commit graph, + * it will return GIT_ENOTFOUND. + * + * This function is not thread-safe. + */ +int git_commit_graph_get_file(git_commit_graph_file **file_out, git_commit_graph *cgraph); + +/* Marks the commit-graph file as needing a refresh. */ +void git_commit_graph_refresh(git_commit_graph *cgraph); + +/* + * A writer for `commit-graph` files. + */ +struct git_commit_graph_writer { + /* + * The path of the `objects/info` directory where the `commit-graph` will be + * stored. + */ + git_str objects_info_dir; + + /* The object ID type of the commit graph. */ + git_oid_t oid_type; + + /* The list of packed commits. */ + git_vector commits; +}; + +int git_commit_graph__writer_dump( + git_str *cgraph, + git_commit_graph_writer *w, + git_commit_graph_writer_options *opts); + +/* + * Returns whether the git_commit_graph_file needs to be reloaded since the + * contents of the commit-graph file have changed on disk. + */ +bool git_commit_graph_file_needs_refresh( + const git_commit_graph_file *file, const char *path); + +int git_commit_graph_entry_find( + git_commit_graph_entry *e, + const git_commit_graph_file *file, + const git_oid *short_oid, + size_t len); +int git_commit_graph_entry_parent( + git_commit_graph_entry *parent, + const git_commit_graph_file *file, + const git_commit_graph_entry *entry, + size_t n); +int git_commit_graph_file_close(git_commit_graph_file *cgraph); +void git_commit_graph_file_free(git_commit_graph_file *cgraph); + +/* This is exposed for use in the fuzzers. */ +int git_commit_graph_file_parse( + git_commit_graph_file *file, + const unsigned char *data, + size_t size); + +#endif diff --git a/src/libgit2/commit_list.c b/src/libgit2/commit_list.c new file mode 100644 index 0000000..7f00c48 --- /dev/null +++ b/src/libgit2/commit_list.c @@ -0,0 +1,219 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "commit_list.h" + +#include "revwalk.h" +#include "pool.h" +#include "odb.h" +#include "commit.h" + +int git_commit_list_generation_cmp(const void *a, const void *b) +{ + uint32_t generation_a = ((git_commit_list_node *) a)->generation; + uint32_t generation_b = ((git_commit_list_node *) b)->generation; + + if (!generation_a || !generation_b) { + /* Fall back to comparing by timestamps if at least one commit lacks a generation. */ + return git_commit_list_time_cmp(a, b); + } + + if (generation_a < generation_b) + return 1; + if (generation_a > generation_b) + return -1; + + return 0; +} + +int git_commit_list_time_cmp(const void *a, const void *b) +{ + int64_t time_a = ((git_commit_list_node *) a)->time; + int64_t time_b = ((git_commit_list_node *) b)->time; + + if (time_a < time_b) + return 1; + if (time_a > time_b) + return -1; + + return 0; +} + +git_commit_list *git_commit_list_create(git_commit_list_node *item, git_commit_list *next) { + git_commit_list *new_list = git__malloc(sizeof(git_commit_list)); + if (new_list != NULL) { + new_list->item = item; + new_list->next = next; + } + return new_list; +} + +git_commit_list *git_commit_list_insert(git_commit_list_node *item, git_commit_list **list_p) +{ + git_commit_list *new_list = git_commit_list_create(item, *list_p); + *list_p = new_list; + return new_list; +} + +git_commit_list *git_commit_list_insert_by_date(git_commit_list_node *item, git_commit_list **list_p) +{ + git_commit_list **pp = list_p; + git_commit_list *p; + + while ((p = *pp) != NULL) { + if (git_commit_list_time_cmp(p->item, item) > 0) + break; + + pp = &p->next; + } + + return git_commit_list_insert(item, pp); +} + +git_commit_list_node *git_commit_list_alloc_node(git_revwalk *walk) +{ + return (git_commit_list_node *)git_pool_mallocz(&walk->commit_pool, 1); +} + +static git_commit_list_node **alloc_parents( + git_revwalk *walk, git_commit_list_node *commit, size_t n_parents) +{ + size_t bytes; + + if (n_parents <= PARENTS_PER_COMMIT) + return (git_commit_list_node **)((char *)commit + sizeof(git_commit_list_node)); + + if (git__multiply_sizet_overflow(&bytes, n_parents, sizeof(git_commit_list_node *))) + return NULL; + + return (git_commit_list_node **)git_pool_malloc(&walk->commit_pool, bytes); +} + + +void git_commit_list_free(git_commit_list **list_p) +{ + git_commit_list *list = *list_p; + + if (list == NULL) + return; + + while (list) { + git_commit_list *temp = list; + list = temp->next; + git__free(temp); + } + + *list_p = NULL; +} + +git_commit_list_node *git_commit_list_pop(git_commit_list **stack) +{ + git_commit_list *top = *stack; + git_commit_list_node *item = top ? top->item : NULL; + + if (top) { + *stack = top->next; + git__free(top); + } + return item; +} + +static int commit_quick_parse( + git_revwalk *walk, + git_commit_list_node *node, + git_odb_object *obj) +{ + git_oid *parent_oid; + git_commit *commit; + git_commit__parse_options parse_opts = { + walk->repo->oid_type, + GIT_COMMIT_PARSE_QUICK + }; + size_t i; + + commit = git__calloc(1, sizeof(*commit)); + GIT_ERROR_CHECK_ALLOC(commit); + commit->object.repo = walk->repo; + + if (git_commit__parse_ext(commit, obj, &parse_opts) < 0) { + git__free(commit); + return -1; + } + + if (!git__is_uint16(git_array_size(commit->parent_ids))) { + git__free(commit); + git_error_set(GIT_ERROR_INVALID, "commit has more than 2^16 parents"); + return -1; + } + + node->generation = 0; + node->time = commit->committer->when.time; + node->out_degree = (uint16_t) git_array_size(commit->parent_ids); + node->parents = alloc_parents(walk, node, node->out_degree); + GIT_ERROR_CHECK_ALLOC(node->parents); + + git_array_foreach(commit->parent_ids, i, parent_oid) { + node->parents[i] = git_revwalk__commit_lookup(walk, parent_oid); + } + + git_commit__free(commit); + + node->parsed = 1; + + return 0; +} + +int git_commit_list_parse(git_revwalk *walk, git_commit_list_node *commit) +{ + git_odb_object *obj; + git_commit_graph_file *cgraph_file = NULL; + int error; + + if (commit->parsed) + return 0; + + /* Let's try to use the commit graph first. */ + git_odb__get_commit_graph_file(&cgraph_file, walk->odb); + if (cgraph_file) { + git_commit_graph_entry e; + + error = git_commit_graph_entry_find(&e, cgraph_file, + &commit->oid, git_oid_size(walk->repo->oid_type)); + + if (error == 0 && git__is_uint16(e.parent_count)) { + size_t i; + commit->generation = (uint32_t)e.generation; + commit->time = e.commit_time; + commit->out_degree = (uint16_t)e.parent_count; + commit->parents = alloc_parents(walk, commit, commit->out_degree); + GIT_ERROR_CHECK_ALLOC(commit->parents); + + for (i = 0; i < commit->out_degree; ++i) { + git_commit_graph_entry parent; + error = git_commit_graph_entry_parent(&parent, cgraph_file, &e, i); + if (error < 0) + return error; + commit->parents[i] = git_revwalk__commit_lookup(walk, &parent.sha1); + } + commit->parsed = 1; + return 0; + } + } + + if ((error = git_odb_read(&obj, walk->odb, &commit->oid)) < 0) + return error; + + if (obj->cached.type != GIT_OBJECT_COMMIT) { + git_error_set(GIT_ERROR_INVALID, "object is no commit object"); + error = -1; + } else + error = commit_quick_parse(walk, commit, obj); + + git_odb_object_free(obj); + return error; +} + diff --git a/src/libgit2/commit_list.h b/src/libgit2/commit_list.h new file mode 100644 index 0000000..e2dbd2a --- /dev/null +++ b/src/libgit2/commit_list.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_commit_list_h__ +#define INCLUDE_commit_list_h__ + +#include "common.h" + +#include "git2/oid.h" + +#define PARENT1 (1 << 0) +#define PARENT2 (1 << 1) +#define RESULT (1 << 2) +#define STALE (1 << 3) +#define ALL_FLAGS (PARENT1 | PARENT2 | STALE | RESULT) + +#define PARENTS_PER_COMMIT 2 +#define COMMIT_ALLOC \ + (sizeof(git_commit_list_node) + PARENTS_PER_COMMIT * sizeof(git_commit_list_node *)) + +#define FLAG_BITS 4 + +typedef struct git_commit_list_node { + git_oid oid; + int64_t time; + uint32_t generation; + unsigned int seen:1, + uninteresting:1, + topo_delay:1, + parsed:1, + added:1, + flags : FLAG_BITS; + + uint16_t in_degree; + uint16_t out_degree; + + struct git_commit_list_node **parents; +} git_commit_list_node; + +typedef struct git_commit_list { + git_commit_list_node *item; + struct git_commit_list *next; +} git_commit_list; + +git_commit_list_node *git_commit_list_alloc_node(git_revwalk *walk); +int git_commit_list_generation_cmp(const void *a, const void *b); +int git_commit_list_time_cmp(const void *a, const void *b); +void git_commit_list_free(git_commit_list **list_p); +git_commit_list *git_commit_list_create(git_commit_list_node *item, git_commit_list *next); +git_commit_list *git_commit_list_insert(git_commit_list_node *item, git_commit_list **list_p); +git_commit_list *git_commit_list_insert_by_date(git_commit_list_node *item, git_commit_list **list_p); +int git_commit_list_parse(git_revwalk *walk, git_commit_list_node *commit); +git_commit_list_node *git_commit_list_pop(git_commit_list **stack); + +#endif diff --git a/src/libgit2/common.h b/src/libgit2/common.h new file mode 100644 index 0000000..bb9ec5a --- /dev/null +++ b/src/libgit2/common.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_common_h__ +#define INCLUDE_common_h__ + +#include "git2_util.h" +#include "errors.h" + +/* +* Include the declarations for deprecated functions; this ensures +* that they're decorated with the proper extern/visibility attributes. +*/ +#include "git2/deprecated.h" + +#include "posix.h" + +/** + * Initialize a structure with a version. + */ +GIT_INLINE(void) git__init_structure(void *structure, size_t len, unsigned int version) +{ + memset(structure, 0, len); + *((int*)structure) = version; +} +#define GIT_INIT_STRUCTURE(S,V) git__init_structure(S, sizeof(*S), V) + +#define GIT_INIT_STRUCTURE_FROM_TEMPLATE(PTR,VERSION,TYPE,TPL) do { \ + TYPE _tmpl = TPL; \ + GIT_ERROR_CHECK_VERSION(&(VERSION), _tmpl.version, #TYPE); \ + memcpy((PTR), &_tmpl, sizeof(_tmpl)); } while (0) + +/** + * Check a versioned structure for validity + */ +GIT_INLINE(int) git_error__check_version(const void *structure, unsigned int expected_max, const char *name) +{ + unsigned int actual; + + if (!structure) + return 0; + + actual = *(const unsigned int*)structure; + if (actual > 0 && actual <= expected_max) + return 0; + + git_error_set(GIT_ERROR_INVALID, "invalid version %d on %s", actual, name); + return -1; +} +#define GIT_ERROR_CHECK_VERSION(S,V,N) if (git_error__check_version(S,V,N) < 0) return -1 + +#endif diff --git a/src/libgit2/config.c b/src/libgit2/config.c new file mode 100644 index 0000000..23a8f9f --- /dev/null +++ b/src/libgit2/config.c @@ -0,0 +1,1576 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "config.h" + +#include "git2/config.h" +#include "git2/sys/config.h" + +#include "buf.h" +#include "config_backend.h" +#include "regexp.h" +#include "sysdir.h" +#include "transaction.h" +#include "vector.h" +#if GIT_WIN32 +# include +#endif + +#include + +void git_config_entry_free(git_config_entry *entry) +{ + if (!entry) + return; + + entry->free(entry); +} + +typedef struct { + git_refcount rc; + + git_config_backend *backend; + git_config_level_t level; +} backend_internal; + +static void backend_internal_free(backend_internal *internal) +{ + git_config_backend *backend; + + backend = internal->backend; + backend->free(backend); + git__free(internal); +} + +static void config_free(git_config *cfg) +{ + size_t i; + backend_internal *internal; + + for (i = 0; i < cfg->backends.length; ++i) { + internal = git_vector_get(&cfg->backends, i); + GIT_REFCOUNT_DEC(internal, backend_internal_free); + } + + git_vector_free(&cfg->backends); + + git__memzero(cfg, sizeof(*cfg)); + git__free(cfg); +} + +void git_config_free(git_config *cfg) +{ + if (cfg == NULL) + return; + + GIT_REFCOUNT_DEC(cfg, config_free); +} + +static int config_backend_cmp(const void *a, const void *b) +{ + const backend_internal *bk_a = (const backend_internal *)(a); + const backend_internal *bk_b = (const backend_internal *)(b); + + return bk_b->level - bk_a->level; +} + +int git_config_new(git_config **out) +{ + git_config *cfg; + + cfg = git__malloc(sizeof(git_config)); + GIT_ERROR_CHECK_ALLOC(cfg); + + memset(cfg, 0x0, sizeof(git_config)); + + if (git_vector_init(&cfg->backends, 3, config_backend_cmp) < 0) { + git__free(cfg); + return -1; + } + + *out = cfg; + GIT_REFCOUNT_INC(cfg); + return 0; +} + +int git_config_add_file_ondisk( + git_config *cfg, + const char *path, + git_config_level_t level, + const git_repository *repo, + int force) +{ + git_config_backend *file = NULL; + struct stat st; + int res; + + GIT_ASSERT_ARG(cfg); + GIT_ASSERT_ARG(path); + + res = p_stat(path, &st); + if (res < 0 && errno != ENOENT && errno != ENOTDIR) { + git_error_set(GIT_ERROR_CONFIG, "failed to stat '%s'", path); + return -1; + } + + if (git_config_backend_from_file(&file, path) < 0) + return -1; + + if ((res = git_config_add_backend(cfg, file, level, repo, force)) < 0) { + /* + * free manually; the file is not owned by the config + * instance yet and will not be freed on cleanup + */ + file->free(file); + return res; + } + + return 0; +} + +int git_config_open_ondisk(git_config **out, const char *path) +{ + int error; + git_config *config; + + *out = NULL; + + if (git_config_new(&config) < 0) + return -1; + + if ((error = git_config_add_file_ondisk(config, path, GIT_CONFIG_LEVEL_LOCAL, NULL, 0)) < 0) + git_config_free(config); + else + *out = config; + + return error; +} + +int git_config_snapshot(git_config **out, git_config *in) +{ + int error = 0; + size_t i; + backend_internal *internal; + git_config *config; + + *out = NULL; + + if (git_config_new(&config) < 0) + return -1; + + git_vector_foreach(&in->backends, i, internal) { + git_config_backend *b; + + if ((error = internal->backend->snapshot(&b, internal->backend)) < 0) + break; + + if ((error = git_config_add_backend(config, b, internal->level, NULL, 0)) < 0) { + b->free(b); + break; + } + } + + if (error < 0) + git_config_free(config); + else + *out = config; + + return error; +} + +static int find_backend_by_level( + backend_internal **out, + const git_config *cfg, + git_config_level_t level) +{ + int pos = -1; + backend_internal *internal; + size_t i; + + /* when passing GIT_CONFIG_HIGHEST_LEVEL, the idea is to get the config backend + * which has the highest level. As config backends are stored in a vector + * sorted by decreasing order of level, getting the backend at position 0 + * will do the job. + */ + if (level == GIT_CONFIG_HIGHEST_LEVEL) { + pos = 0; + } else { + git_vector_foreach(&cfg->backends, i, internal) { + if (internal->level == level) + pos = (int)i; + } + } + + if (pos == -1) { + git_error_set(GIT_ERROR_CONFIG, + "no configuration exists for the given level '%i'", (int)level); + return GIT_ENOTFOUND; + } + + *out = git_vector_get(&cfg->backends, pos); + + return 0; +} + +static int duplicate_level(void **old_raw, void *new_raw) +{ + backend_internal **old = (backend_internal **)old_raw; + + GIT_UNUSED(new_raw); + + git_error_set(GIT_ERROR_CONFIG, "there already exists a configuration for the given level (%i)", (int)(*old)->level); + return GIT_EEXISTS; +} + +static void try_remove_existing_backend( + git_config *cfg, + git_config_level_t level) +{ + int pos = -1; + backend_internal *internal; + size_t i; + + git_vector_foreach(&cfg->backends, i, internal) { + if (internal->level == level) + pos = (int)i; + } + + if (pos == -1) + return; + + internal = git_vector_get(&cfg->backends, pos); + + if (git_vector_remove(&cfg->backends, pos) < 0) + return; + + GIT_REFCOUNT_DEC(internal, backend_internal_free); +} + +static int git_config__add_internal( + git_config *cfg, + backend_internal *internal, + git_config_level_t level, + int force) +{ + int result; + + /* delete existing config backend for level if it exists */ + if (force) + try_remove_existing_backend(cfg, level); + + if ((result = git_vector_insert_sorted(&cfg->backends, + internal, &duplicate_level)) < 0) + return result; + + git_vector_sort(&cfg->backends); + internal->backend->cfg = cfg; + + GIT_REFCOUNT_INC(internal); + + return 0; +} + +int git_config_open_global(git_config **cfg_out, git_config *cfg) +{ + if (!git_config_open_level(cfg_out, cfg, GIT_CONFIG_LEVEL_XDG)) + return 0; + + return git_config_open_level(cfg_out, cfg, GIT_CONFIG_LEVEL_GLOBAL); +} + +int git_config_open_level( + git_config **cfg_out, + const git_config *cfg_parent, + git_config_level_t level) +{ + git_config *cfg; + backend_internal *internal; + int res; + + if ((res = find_backend_by_level(&internal, cfg_parent, level)) < 0) + return res; + + if ((res = git_config_new(&cfg)) < 0) + return res; + + if ((res = git_config__add_internal(cfg, internal, level, true)) < 0) { + git_config_free(cfg); + return res; + } + + *cfg_out = cfg; + + return 0; +} + +int git_config_add_backend( + git_config *cfg, + git_config_backend *backend, + git_config_level_t level, + const git_repository *repo, + int force) +{ + backend_internal *internal; + int result; + + GIT_ASSERT_ARG(cfg); + GIT_ASSERT_ARG(backend); + + GIT_ERROR_CHECK_VERSION(backend, GIT_CONFIG_BACKEND_VERSION, "git_config_backend"); + + if ((result = backend->open(backend, level, repo)) < 0) + return result; + + internal = git__malloc(sizeof(backend_internal)); + GIT_ERROR_CHECK_ALLOC(internal); + + memset(internal, 0x0, sizeof(backend_internal)); + + internal->backend = backend; + internal->level = level; + + if ((result = git_config__add_internal(cfg, internal, level, force)) < 0) { + git__free(internal); + return result; + } + + return 0; +} + +/* + * Loop over all the variables + */ + +typedef struct { + git_config_iterator parent; + git_config_iterator *current; + const git_config *cfg; + git_regexp regex; + size_t i; +} all_iter; + +static int find_next_backend(size_t *out, const git_config *cfg, size_t i) +{ + backend_internal *internal; + + for (; i > 0; --i) { + internal = git_vector_get(&cfg->backends, i - 1); + if (!internal || !internal->backend) + continue; + + *out = i; + return 0; + } + + return -1; +} + +static int all_iter_next(git_config_entry **entry, git_config_iterator *_iter) +{ + all_iter *iter = (all_iter *) _iter; + backend_internal *internal; + git_config_backend *backend; + size_t i; + int error = 0; + + if (iter->current != NULL && + (error = iter->current->next(entry, iter->current)) == 0) { + return 0; + } + + if (error < 0 && error != GIT_ITEROVER) + return error; + + do { + if (find_next_backend(&i, iter->cfg, iter->i) < 0) + return GIT_ITEROVER; + + internal = git_vector_get(&iter->cfg->backends, i - 1); + backend = internal->backend; + iter->i = i - 1; + + if (iter->current) + iter->current->free(iter->current); + + iter->current = NULL; + error = backend->iterator(&iter->current, backend); + if (error == GIT_ENOTFOUND) + continue; + + if (error < 0) + return error; + + error = iter->current->next(entry, iter->current); + /* If this backend is empty, then keep going */ + if (error == GIT_ITEROVER) + continue; + + return error; + + } while(1); + + return GIT_ITEROVER; +} + +static int all_iter_glob_next(git_config_entry **entry, git_config_iterator *_iter) +{ + int error; + all_iter *iter = (all_iter *) _iter; + + /* + * We use the "normal" function to grab the next one across + * backends and then apply the regex + */ + while ((error = all_iter_next(entry, _iter)) == 0) { + /* skip non-matching keys if regexp was provided */ + if (git_regexp_match(&iter->regex, (*entry)->name) != 0) + continue; + + /* and simply return if we like the entry's name */ + return 0; + } + + return error; +} + +static void all_iter_free(git_config_iterator *_iter) +{ + all_iter *iter = (all_iter *) _iter; + + if (iter->current) + iter->current->free(iter->current); + + git__free(iter); +} + +static void all_iter_glob_free(git_config_iterator *_iter) +{ + all_iter *iter = (all_iter *) _iter; + + git_regexp_dispose(&iter->regex); + all_iter_free(_iter); +} + +int git_config_iterator_new(git_config_iterator **out, const git_config *cfg) +{ + all_iter *iter; + + iter = git__calloc(1, sizeof(all_iter)); + GIT_ERROR_CHECK_ALLOC(iter); + + iter->parent.free = all_iter_free; + iter->parent.next = all_iter_next; + + iter->i = cfg->backends.length; + iter->cfg = cfg; + + *out = (git_config_iterator *) iter; + + return 0; +} + +int git_config_iterator_glob_new(git_config_iterator **out, const git_config *cfg, const char *regexp) +{ + all_iter *iter; + int result; + + if (regexp == NULL) + return git_config_iterator_new(out, cfg); + + iter = git__calloc(1, sizeof(all_iter)); + GIT_ERROR_CHECK_ALLOC(iter); + + if ((result = git_regexp_compile(&iter->regex, regexp, 0)) < 0) { + git__free(iter); + return -1; + } + + iter->parent.next = all_iter_glob_next; + iter->parent.free = all_iter_glob_free; + iter->i = cfg->backends.length; + iter->cfg = cfg; + + *out = (git_config_iterator *) iter; + + return 0; +} + +int git_config_foreach( + const git_config *cfg, git_config_foreach_cb cb, void *payload) +{ + return git_config_foreach_match(cfg, NULL, cb, payload); +} + +int git_config_backend_foreach_match( + git_config_backend *backend, + const char *regexp, + git_config_foreach_cb cb, + void *payload) +{ + git_config_entry *entry; + git_config_iterator *iter; + git_regexp regex; + int error = 0; + + GIT_ASSERT_ARG(backend); + GIT_ASSERT_ARG(cb); + + if (regexp && git_regexp_compile(®ex, regexp, 0) < 0) + return -1; + + if ((error = backend->iterator(&iter, backend)) < 0) { + iter = NULL; + return -1; + } + + while (!(iter->next(&entry, iter) < 0)) { + /* skip non-matching keys if regexp was provided */ + if (regexp && git_regexp_match(®ex, entry->name) != 0) + continue; + + /* abort iterator on non-zero return value */ + if ((error = cb(entry, payload)) != 0) { + git_error_set_after_callback(error); + break; + } + } + + if (regexp != NULL) + git_regexp_dispose(®ex); + + iter->free(iter); + + return error; +} + +int git_config_foreach_match( + const git_config *cfg, + const char *regexp, + git_config_foreach_cb cb, + void *payload) +{ + int error; + git_config_iterator *iter; + git_config_entry *entry; + + if ((error = git_config_iterator_glob_new(&iter, cfg, regexp)) < 0) + return error; + + while (!(error = git_config_next(&entry, iter))) { + if ((error = cb(entry, payload)) != 0) { + git_error_set_after_callback(error); + break; + } + } + + git_config_iterator_free(iter); + + if (error == GIT_ITEROVER) + error = 0; + + return error; +} + +/************** + * Setters + **************/ + +typedef enum { + BACKEND_USE_SET, + BACKEND_USE_DELETE +} backend_use; + +static const char *uses[] = { + "set", + "delete" +}; + +static int get_backend_for_use(git_config_backend **out, + git_config *cfg, const char *name, backend_use use) +{ + size_t i; + backend_internal *backend; + + *out = NULL; + + if (git_vector_length(&cfg->backends) == 0) { + git_error_set(GIT_ERROR_CONFIG, + "cannot %s value for '%s' when no config backends exist", + uses[use], name); + return GIT_ENOTFOUND; + } + + git_vector_foreach(&cfg->backends, i, backend) { + if (!backend->backend->readonly) { + *out = backend->backend; + return 0; + } + } + + git_error_set(GIT_ERROR_CONFIG, + "cannot %s value for '%s' when all config backends are readonly", + uses[use], name); + return GIT_ENOTFOUND; +} + +int git_config_delete_entry(git_config *cfg, const char *name) +{ + git_config_backend *backend; + + if (get_backend_for_use(&backend, cfg, name, BACKEND_USE_DELETE) < 0) + return GIT_ENOTFOUND; + + return backend->del(backend, name); +} + +int git_config_set_int64(git_config *cfg, const char *name, int64_t value) +{ + char str_value[32]; /* All numbers should fit in here */ + p_snprintf(str_value, sizeof(str_value), "%" PRId64, value); + return git_config_set_string(cfg, name, str_value); +} + +int git_config_set_int32(git_config *cfg, const char *name, int32_t value) +{ + return git_config_set_int64(cfg, name, (int64_t)value); +} + +int git_config_set_bool(git_config *cfg, const char *name, int value) +{ + return git_config_set_string(cfg, name, value ? "true" : "false"); +} + +int git_config_set_string(git_config *cfg, const char *name, const char *value) +{ + int error; + git_config_backend *backend; + + if (!value) { + git_error_set(GIT_ERROR_CONFIG, "the value to set cannot be NULL"); + return -1; + } + + if (get_backend_for_use(&backend, cfg, name, BACKEND_USE_SET) < 0) + return GIT_ENOTFOUND; + + error = backend->set(backend, name, value); + + if (!error && GIT_REFCOUNT_OWNER(cfg) != NULL) + git_repository__configmap_lookup_cache_clear(GIT_REFCOUNT_OWNER(cfg)); + + return error; +} + +int git_config__update_entry( + git_config *config, + const char *key, + const char *value, + bool overwrite_existing, + bool only_if_existing) +{ + int error = 0; + git_config_entry *ce = NULL; + + if ((error = git_config__lookup_entry(&ce, config, key, false)) < 0) + return error; + + if (!ce && only_if_existing) /* entry doesn't exist */ + return 0; + if (ce && !overwrite_existing) /* entry would be overwritten */ + return 0; + if (value && ce && ce->value && !strcmp(ce->value, value)) /* no change */ + return 0; + if (!value && (!ce || !ce->value)) /* asked to delete absent entry */ + return 0; + + if (!value) + error = git_config_delete_entry(config, key); + else + error = git_config_set_string(config, key, value); + + git_config_entry_free(ce); + return error; +} + +/*********** + * Getters + ***********/ + +static int config_error_notfound(const char *name) +{ + git_error_set(GIT_ERROR_CONFIG, "config value '%s' was not found", name); + return GIT_ENOTFOUND; +} + +enum { + GET_ALL_ERRORS = 0, + GET_NO_MISSING = 1, + GET_NO_ERRORS = 2 +}; + +static int get_entry( + git_config_entry **out, + const git_config *cfg, + const char *name, + bool normalize_name, + int want_errors) +{ + int res = GIT_ENOTFOUND; + const char *key = name; + char *normalized = NULL; + size_t i; + backend_internal *internal; + + *out = NULL; + + if (normalize_name) { + if ((res = git_config__normalize_name(name, &normalized)) < 0) + goto cleanup; + key = normalized; + } + + res = GIT_ENOTFOUND; + git_vector_foreach(&cfg->backends, i, internal) { + if (!internal || !internal->backend) + continue; + + res = internal->backend->get(internal->backend, key, out); + if (res != GIT_ENOTFOUND) + break; + } + + git__free(normalized); + +cleanup: + if (res == GIT_ENOTFOUND) + res = (want_errors > GET_ALL_ERRORS) ? 0 : config_error_notfound(name); + else if (res && (want_errors == GET_NO_ERRORS)) { + git_error_clear(); + res = 0; + } + + return res; +} + +int git_config_get_entry( + git_config_entry **out, const git_config *cfg, const char *name) +{ + return get_entry(out, cfg, name, true, GET_ALL_ERRORS); +} + +int git_config__lookup_entry( + git_config_entry **out, + const git_config *cfg, + const char *key, + bool no_errors) +{ + return get_entry( + out, cfg, key, false, no_errors ? GET_NO_ERRORS : GET_NO_MISSING); +} + +int git_config_get_mapped( + int *out, + const git_config *cfg, + const char *name, + const git_configmap *maps, + size_t map_n) +{ + git_config_entry *entry; + int ret; + + if ((ret = get_entry(&entry, cfg, name, true, GET_ALL_ERRORS)) < 0) + return ret; + + ret = git_config_lookup_map_value(out, maps, map_n, entry->value); + git_config_entry_free(entry); + + return ret; +} + +int git_config_get_int64(int64_t *out, const git_config *cfg, const char *name) +{ + git_config_entry *entry; + int ret; + + if ((ret = get_entry(&entry, cfg, name, true, GET_ALL_ERRORS)) < 0) + return ret; + + ret = git_config_parse_int64(out, entry->value); + git_config_entry_free(entry); + + return ret; +} + +int git_config_get_int32(int32_t *out, const git_config *cfg, const char *name) +{ + git_config_entry *entry; + int ret; + + if ((ret = get_entry(&entry, cfg, name, true, GET_ALL_ERRORS)) < 0) + return ret; + + ret = git_config_parse_int32(out, entry->value); + git_config_entry_free(entry); + + return ret; +} + +int git_config_get_bool(int *out, const git_config *cfg, const char *name) +{ + git_config_entry *entry; + int ret; + + if ((ret = get_entry(&entry, cfg, name, true, GET_ALL_ERRORS)) < 0) + return ret; + + ret = git_config_parse_bool(out, entry->value); + git_config_entry_free(entry); + + return ret; +} + +static int is_readonly(const git_config *cfg) +{ + size_t i; + backend_internal *internal; + + git_vector_foreach(&cfg->backends, i, internal) { + if (!internal || !internal->backend) + continue; + + if (!internal->backend->readonly) + return 0; + } + + return 1; +} + +static int git_config__parse_path(git_str *out, const char *value) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(value); + + if (value[0] == '~') { + if (value[1] != '\0' && value[1] != '/') { + git_error_set(GIT_ERROR_CONFIG, "retrieving a homedir by name is not supported"); + return -1; + } + + return git_sysdir_expand_homedir_file(out, value[1] ? &value[2] : NULL); + } + + return git_str_sets(out, value); +} + +int git_config_parse_path(git_buf *out, const char *value) +{ + GIT_BUF_WRAP_PRIVATE(out, git_config__parse_path, value); +} + +int git_config_get_path( + git_buf *out, + const git_config *cfg, + const char *name) +{ + GIT_BUF_WRAP_PRIVATE(out, git_config__get_path, cfg, name); +} + +int git_config__get_path( + git_str *out, + const git_config *cfg, + const char *name) +{ + git_config_entry *entry; + int error; + + if ((error = get_entry(&entry, cfg, name, true, GET_ALL_ERRORS)) < 0) + return error; + + error = git_config__parse_path(out, entry->value); + git_config_entry_free(entry); + + return error; +} + +int git_config_get_string( + const char **out, const git_config *cfg, const char *name) +{ + git_config_entry *entry; + int ret; + + if (!is_readonly(cfg)) { + git_error_set(GIT_ERROR_CONFIG, "get_string called on a live config object"); + return -1; + } + + ret = get_entry(&entry, cfg, name, true, GET_ALL_ERRORS); + *out = !ret ? (entry->value ? entry->value : "") : NULL; + + git_config_entry_free(entry); + + return ret; +} + +int git_config_get_string_buf( + git_buf *out, const git_config *cfg, const char *name) +{ + GIT_BUF_WRAP_PRIVATE(out, git_config__get_string_buf, cfg, name); +} + +int git_config__get_string_buf( + git_str *out, const git_config *cfg, const char *name) +{ + git_config_entry *entry; + int ret; + const char *str; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(cfg); + + ret = get_entry(&entry, cfg, name, true, GET_ALL_ERRORS); + str = !ret ? (entry->value ? entry->value : "") : NULL; + + if (str) + ret = git_str_puts(out, str); + + git_config_entry_free(entry); + + return ret; +} + +char *git_config__get_string_force( + const git_config *cfg, const char *key, const char *fallback_value) +{ + git_config_entry *entry; + char *ret; + + get_entry(&entry, cfg, key, false, GET_NO_ERRORS); + ret = (entry && entry->value) ? git__strdup(entry->value) : fallback_value ? git__strdup(fallback_value) : NULL; + git_config_entry_free(entry); + + return ret; +} + +int git_config__get_bool_force( + const git_config *cfg, const char *key, int fallback_value) +{ + int val = fallback_value; + git_config_entry *entry; + + get_entry(&entry, cfg, key, false, GET_NO_ERRORS); + + if (entry && git_config_parse_bool(&val, entry->value) < 0) + git_error_clear(); + + git_config_entry_free(entry); + return val; +} + +int git_config__get_int_force( + const git_config *cfg, const char *key, int fallback_value) +{ + int32_t val = (int32_t)fallback_value; + git_config_entry *entry; + + get_entry(&entry, cfg, key, false, GET_NO_ERRORS); + + if (entry && git_config_parse_int32(&val, entry->value) < 0) + git_error_clear(); + + git_config_entry_free(entry); + return (int)val; +} + +int git_config_get_multivar_foreach( + const git_config *cfg, const char *name, const char *regexp, + git_config_foreach_cb cb, void *payload) +{ + int err, found; + git_config_iterator *iter; + git_config_entry *entry; + + if ((err = git_config_multivar_iterator_new(&iter, cfg, name, regexp)) < 0) + return err; + + found = 0; + while ((err = iter->next(&entry, iter)) == 0) { + found = 1; + + if ((err = cb(entry, payload)) != 0) { + git_error_set_after_callback(err); + break; + } + } + + iter->free(iter); + if (err == GIT_ITEROVER) + err = 0; + + if (found == 0 && err == 0) + err = config_error_notfound(name); + + return err; +} + +typedef struct { + git_config_iterator parent; + git_config_iterator *iter; + char *name; + git_regexp regex; + int have_regex; +} multivar_iter; + +static int multivar_iter_next(git_config_entry **entry, git_config_iterator *_iter) +{ + multivar_iter *iter = (multivar_iter *) _iter; + int error = 0; + + while ((error = iter->iter->next(entry, iter->iter)) == 0) { + if (git__strcmp(iter->name, (*entry)->name)) + continue; + + if (!iter->have_regex) + return 0; + + if (git_regexp_match(&iter->regex, (*entry)->value) == 0) + return 0; + } + + return error; +} + +static void multivar_iter_free(git_config_iterator *_iter) +{ + multivar_iter *iter = (multivar_iter *) _iter; + + iter->iter->free(iter->iter); + + git__free(iter->name); + if (iter->have_regex) + git_regexp_dispose(&iter->regex); + git__free(iter); +} + +int git_config_multivar_iterator_new(git_config_iterator **out, const git_config *cfg, const char *name, const char *regexp) +{ + multivar_iter *iter = NULL; + git_config_iterator *inner = NULL; + int error; + + if ((error = git_config_iterator_new(&inner, cfg)) < 0) + return error; + + iter = git__calloc(1, sizeof(multivar_iter)); + GIT_ERROR_CHECK_ALLOC(iter); + + if ((error = git_config__normalize_name(name, &iter->name)) < 0) + goto on_error; + + if (regexp != NULL) { + if ((error = git_regexp_compile(&iter->regex, regexp, 0)) < 0) + goto on_error; + + iter->have_regex = 1; + } + + iter->iter = inner; + iter->parent.free = multivar_iter_free; + iter->parent.next = multivar_iter_next; + + *out = (git_config_iterator *) iter; + + return 0; + +on_error: + + inner->free(inner); + git__free(iter); + return error; +} + +int git_config_set_multivar(git_config *cfg, const char *name, const char *regexp, const char *value) +{ + git_config_backend *backend; + + if (get_backend_for_use(&backend, cfg, name, BACKEND_USE_DELETE) < 0) + return GIT_ENOTFOUND; + + return backend->set_multivar(backend, name, regexp, value); +} + +int git_config_delete_multivar(git_config *cfg, const char *name, const char *regexp) +{ + git_config_backend *backend; + + if (get_backend_for_use(&backend, cfg, name, BACKEND_USE_DELETE) < 0) + return GIT_ENOTFOUND; + + return backend->del_multivar(backend, name, regexp); +} + +int git_config_next(git_config_entry **entry, git_config_iterator *iter) +{ + return iter->next(entry, iter); +} + +void git_config_iterator_free(git_config_iterator *iter) +{ + if (iter == NULL) + return; + + iter->free(iter); +} + +int git_config_find_global(git_buf *path) +{ + GIT_BUF_WRAP_PRIVATE(path, git_sysdir_find_global_file, GIT_CONFIG_FILENAME_GLOBAL); +} + +int git_config__find_global(git_str *path) +{ + return git_sysdir_find_global_file(path, GIT_CONFIG_FILENAME_GLOBAL); +} + +int git_config_find_xdg(git_buf *path) +{ + GIT_BUF_WRAP_PRIVATE(path, git_sysdir_find_xdg_file, GIT_CONFIG_FILENAME_XDG); +} + +int git_config__find_xdg(git_str *path) +{ + return git_sysdir_find_xdg_file(path, GIT_CONFIG_FILENAME_XDG); +} + +int git_config_find_system(git_buf *path) +{ + GIT_BUF_WRAP_PRIVATE(path, git_sysdir_find_system_file, GIT_CONFIG_FILENAME_SYSTEM); +} + +int git_config__find_system(git_str *path) +{ + return git_sysdir_find_system_file(path, GIT_CONFIG_FILENAME_SYSTEM); +} + +int git_config_find_programdata(git_buf *path) +{ + git_str str = GIT_STR_INIT; + int error; + + if ((error = git_buf_tostr(&str, path)) == 0 && + (error = git_config__find_programdata(&str)) == 0) + error = git_buf_fromstr(path, &str); + + git_str_dispose(&str); + return error; +} + +int git_config__find_programdata(git_str *path) +{ + git_fs_path_owner_t owner_level = + GIT_FS_PATH_OWNER_CURRENT_USER | + GIT_FS_PATH_OWNER_ADMINISTRATOR; + bool is_safe; + int error; + + if ((error = git_sysdir_find_programdata_file(path, GIT_CONFIG_FILENAME_PROGRAMDATA)) < 0) + return error; + + if (git_fs_path_owner_is(&is_safe, path->ptr, owner_level) < 0) + return -1; + + if (!is_safe) { + git_error_set(GIT_ERROR_CONFIG, "programdata path has invalid ownership"); + return -1; + } + + return 0; +} + +int git_config__global_location(git_str *buf) +{ + const git_str *paths; + const char *sep, *start; + + if (git_sysdir_get(&paths, GIT_SYSDIR_GLOBAL) < 0) + return -1; + + /* no paths, so give up */ + if (!paths || !git_str_len(paths)) + return -1; + + /* find unescaped separator or end of string */ + for (sep = start = git_str_cstr(paths); *sep; ++sep) { + if (*sep == GIT_PATH_LIST_SEPARATOR && + (sep <= start || sep[-1] != '\\')) + break; + } + + if (git_str_set(buf, start, (size_t)(sep - start)) < 0) + return -1; + + return git_str_joinpath(buf, buf->ptr, GIT_CONFIG_FILENAME_GLOBAL); +} + +int git_config_open_default(git_config **out) +{ + int error; + git_config *cfg = NULL; + git_str buf = GIT_STR_INIT; + + if ((error = git_config_new(&cfg)) < 0) + return error; + + if (!git_config__find_global(&buf) || + !git_config__global_location(&buf)) { + error = git_config_add_file_ondisk(cfg, buf.ptr, + GIT_CONFIG_LEVEL_GLOBAL, NULL, 0); + } + + if (!error && !git_config__find_xdg(&buf)) + error = git_config_add_file_ondisk(cfg, buf.ptr, + GIT_CONFIG_LEVEL_XDG, NULL, 0); + + if (!error && !git_config__find_system(&buf)) + error = git_config_add_file_ondisk(cfg, buf.ptr, + GIT_CONFIG_LEVEL_SYSTEM, NULL, 0); + + if (!error && !git_config__find_programdata(&buf)) + error = git_config_add_file_ondisk(cfg, buf.ptr, + GIT_CONFIG_LEVEL_PROGRAMDATA, NULL, 0); + + git_str_dispose(&buf); + + if (error) { + git_config_free(cfg); + cfg = NULL; + } + + *out = cfg; + + return error; +} + +int git_config_lock(git_transaction **out, git_config *cfg) +{ + int error; + git_config_backend *backend; + backend_internal *internal; + + GIT_ASSERT_ARG(cfg); + + internal = git_vector_get(&cfg->backends, 0); + if (!internal || !internal->backend) { + git_error_set(GIT_ERROR_CONFIG, "cannot lock; the config has no backends"); + return -1; + } + backend = internal->backend; + + if ((error = backend->lock(backend)) < 0) + return error; + + return git_transaction_config_new(out, cfg); +} + +int git_config_unlock(git_config *cfg, int commit) +{ + git_config_backend *backend; + backend_internal *internal; + + GIT_ASSERT_ARG(cfg); + + internal = git_vector_get(&cfg->backends, 0); + if (!internal || !internal->backend) { + git_error_set(GIT_ERROR_CONFIG, "cannot lock; the config has no backends"); + return -1; + } + + backend = internal->backend; + + return backend->unlock(backend, commit); +} + +/*********** + * Parsers + ***********/ + +int git_config_lookup_map_value( + int *out, + const git_configmap *maps, + size_t map_n, + const char *value) +{ + size_t i; + + for (i = 0; i < map_n; ++i) { + const git_configmap *m = maps + i; + + switch (m->type) { + case GIT_CONFIGMAP_FALSE: + case GIT_CONFIGMAP_TRUE: { + int bool_val; + + if (git_config_parse_bool(&bool_val, value) == 0 && + bool_val == (int)m->type) { + *out = m->map_value; + return 0; + } + break; + } + + case GIT_CONFIGMAP_INT32: + if (git_config_parse_int32(out, value) == 0) + return 0; + break; + + case GIT_CONFIGMAP_STRING: + if (value && strcasecmp(value, m->str_match) == 0) { + *out = m->map_value; + return 0; + } + break; + } + } + + git_error_set(GIT_ERROR_CONFIG, "failed to map '%s'", value); + return -1; +} + +int git_config_lookup_map_enum(git_configmap_t *type_out, const char **str_out, + const git_configmap *maps, size_t map_n, int enum_val) +{ + size_t i; + + for (i = 0; i < map_n; i++) { + const git_configmap *m = &maps[i]; + + if (m->map_value != enum_val) + continue; + + *type_out = m->type; + *str_out = m->str_match; + return 0; + } + + git_error_set(GIT_ERROR_CONFIG, "invalid enum value"); + return GIT_ENOTFOUND; +} + +int git_config_parse_bool(int *out, const char *value) +{ + if (git__parse_bool(out, value) == 0) + return 0; + + if (git_config_parse_int32(out, value) == 0) { + *out = !!(*out); + return 0; + } + + git_error_set(GIT_ERROR_CONFIG, "failed to parse '%s' as a boolean value", value); + return -1; +} + +int git_config_parse_int64(int64_t *out, const char *value) +{ + const char *num_end; + int64_t num; + + if (!value || git__strntol64(&num, value, strlen(value), &num_end, 0) < 0) + goto fail_parse; + + switch (*num_end) { + case 'g': + case 'G': + num *= 1024; + /* fallthrough */ + + case 'm': + case 'M': + num *= 1024; + /* fallthrough */ + + case 'k': + case 'K': + num *= 1024; + + /* check that that there are no more characters after the + * given modifier suffix */ + if (num_end[1] != '\0') + return -1; + + /* fallthrough */ + + case '\0': + *out = num; + return 0; + + default: + goto fail_parse; + } + +fail_parse: + git_error_set(GIT_ERROR_CONFIG, "failed to parse '%s' as an integer", value ? value : "(null)"); + return -1; +} + +int git_config_parse_int32(int32_t *out, const char *value) +{ + int64_t tmp; + int32_t truncate; + + if (git_config_parse_int64(&tmp, value) < 0) + goto fail_parse; + + truncate = tmp & 0xFFFFFFFF; + if (truncate != tmp) + goto fail_parse; + + *out = truncate; + return 0; + +fail_parse: + git_error_set(GIT_ERROR_CONFIG, "failed to parse '%s' as a 32-bit integer", value ? value : "(null)"); + return -1; +} + +static int normalize_section(char *start, char *end) +{ + char *scan; + + if (start == end) + return GIT_EINVALIDSPEC; + + /* Validate and downcase range */ + for (scan = start; *scan; ++scan) { + if (end && scan >= end) + break; + if (isalnum(*scan)) + *scan = (char)git__tolower(*scan); + else if (*scan != '-' || scan == start) + return GIT_EINVALIDSPEC; + } + + if (scan == start) + return GIT_EINVALIDSPEC; + + return 0; +} + + +/* Take something the user gave us and make it nice for our hash function */ +int git_config__normalize_name(const char *in, char **out) +{ + char *name, *fdot, *ldot; + + GIT_ASSERT_ARG(in); + GIT_ASSERT_ARG(out); + + name = git__strdup(in); + GIT_ERROR_CHECK_ALLOC(name); + + fdot = strchr(name, '.'); + ldot = strrchr(name, '.'); + + if (fdot == NULL || fdot == name || ldot == NULL || !ldot[1]) + goto invalid; + + /* Validate and downcase up to first dot and after last dot */ + if (normalize_section(name, fdot) < 0 || + normalize_section(ldot + 1, NULL) < 0) + goto invalid; + + /* If there is a middle range, make sure it doesn't have newlines */ + while (fdot < ldot) + if (*fdot++ == '\n') + goto invalid; + + *out = name; + return 0; + +invalid: + git__free(name); + git_error_set(GIT_ERROR_CONFIG, "invalid config item name '%s'", in); + return GIT_EINVALIDSPEC; +} + +struct rename_data { + git_config *config; + git_str *name; + size_t old_len; +}; + +static int rename_config_entries_cb( + const git_config_entry *entry, + void *payload) +{ + int error = 0; + struct rename_data *data = (struct rename_data *)payload; + size_t base_len = git_str_len(data->name); + + if (base_len > 0 && + !(error = git_str_puts(data->name, entry->name + data->old_len))) + { + error = git_config_set_string( + data->config, git_str_cstr(data->name), entry->value); + + git_str_truncate(data->name, base_len); + } + + if (!error) + error = git_config_delete_entry(data->config, entry->name); + + return error; +} + +int git_config_rename_section( + git_repository *repo, + const char *old_section_name, + const char *new_section_name) +{ + git_config *config; + git_str pattern = GIT_STR_INIT, replace = GIT_STR_INIT; + int error = 0; + struct rename_data data; + + git_str_puts_escape_regex(&pattern, old_section_name); + + if ((error = git_str_puts(&pattern, "\\..+")) < 0) + goto cleanup; + + if ((error = git_repository_config__weakptr(&config, repo)) < 0) + goto cleanup; + + data.config = config; + data.name = &replace; + data.old_len = strlen(old_section_name) + 1; + + if ((error = git_str_join(&replace, '.', new_section_name, "")) < 0) + goto cleanup; + + if (new_section_name != NULL && + (error = normalize_section(replace.ptr, strchr(replace.ptr, '.'))) < 0) + { + git_error_set( + GIT_ERROR_CONFIG, "invalid config section '%s'", new_section_name); + goto cleanup; + } + + error = git_config_foreach_match( + config, git_str_cstr(&pattern), rename_config_entries_cb, &data); + +cleanup: + git_str_dispose(&pattern); + git_str_dispose(&replace); + + return error; +} + +int git_config_init_backend(git_config_backend *backend, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + backend, version, git_config_backend, GIT_CONFIG_BACKEND_INIT); + return 0; +} diff --git a/src/libgit2/config.h b/src/libgit2/config.h new file mode 100644 index 0000000..01b84b1 --- /dev/null +++ b/src/libgit2/config.h @@ -0,0 +1,110 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_config_h__ +#define INCLUDE_config_h__ + +#include "common.h" + +#include "git2.h" +#include "git2/config.h" +#include "vector.h" +#include "repository.h" + +#define GIT_CONFIG_FILENAME_PROGRAMDATA "config" +#define GIT_CONFIG_FILENAME_SYSTEM "gitconfig" +#define GIT_CONFIG_FILENAME_GLOBAL ".gitconfig" +#define GIT_CONFIG_FILENAME_XDG "config" + +#define GIT_CONFIG_FILENAME_INREPO "config" +#define GIT_CONFIG_FILE_MODE 0666 + +struct git_config { + git_refcount rc; + git_vector backends; +}; + +extern int git_config__global_location(git_str *buf); + +extern int git_config__find_global(git_str *path); +extern int git_config__find_xdg(git_str *path); +extern int git_config__find_system(git_str *path); +extern int git_config__find_programdata(git_str *path); + +extern int git_config_rename_section( + git_repository *repo, + const char *old_section_name, /* eg "branch.dummy" */ + const char *new_section_name); /* NULL to drop the old section */ + +extern int git_config__normalize_name(const char *in, char **out); + +/* internal only: does not normalize key and sets out to NULL if not found */ +extern int git_config__lookup_entry( + git_config_entry **out, + const git_config *cfg, + const char *key, + bool no_errors); + +/* internal only: update and/or delete entry string with constraints */ +extern int git_config__update_entry( + git_config *cfg, + const char *key, + const char *value, + bool overwrite_existing, + bool only_if_existing); + +int git_config__get_path( + git_str *out, + const git_config *cfg, + const char *name); + +int git_config__get_string_buf( + git_str *out, const git_config *cfg, const char *name); + +/* + * Lookup functions that cannot fail. These functions look up a config + * value and return a fallback value if the value is missing or if any + * failures occur while trying to access the value. + */ + +extern char *git_config__get_string_force( + const git_config *cfg, const char *key, const char *fallback_value); + +extern int git_config__get_bool_force( + const git_config *cfg, const char *key, int fallback_value); + +extern int git_config__get_int_force( + const git_config *cfg, const char *key, int fallback_value); + +/* API for repository configmap-style lookups from config - not cached, but + * uses configmap value maps and fallbacks + */ +extern int git_config__configmap_lookup( + int *out, git_config *config, git_configmap_item item); + +/** + * The opposite of git_config_lookup_map_value, we take an enum value + * and map it to the string or bool value on the config. + */ +int git_config_lookup_map_enum(git_configmap_t *type_out, + const char **str_out, const git_configmap *maps, + size_t map_n, int enum_val); + +/** + * Unlock the backend with the highest priority + * + * Unlocking will allow other writers to update the configuration + * file. Optionally, any changes performed since the lock will be + * applied to the configuration. + * + * @param cfg the configuration + * @param commit boolean which indicates whether to commit any changes + * done since locking + * @return 0 or an error code + */ +GIT_EXTERN(int) git_config_unlock(git_config *cfg, int commit); + +#endif diff --git a/src/libgit2/config_backend.h b/src/libgit2/config_backend.h new file mode 100644 index 0000000..dbb1905 --- /dev/null +++ b/src/libgit2/config_backend.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_config_file_h__ +#define INCLUDE_config_file_h__ + +#include "common.h" + +#include "git2/sys/config.h" +#include "git2/config.h" + +/** + * Create a configuration file backend for ondisk files + * + * These are the normal `.gitconfig` files that Core Git + * processes. Note that you first have to add this file to a + * configuration object before you can query it for configuration + * variables. + * + * @param out the new backend + * @param path where the config file is located + */ +extern int git_config_backend_from_file(git_config_backend **out, const char *path); + +/** + * Create a readonly configuration file backend from another backend + * + * This copies the complete contents of the source backend to the + * new backend. The new backend will be completely read-only and + * cannot be modified. + * + * @param out the new snapshotted backend + * @param source the backend to copy + */ +extern int git_config_backend_snapshot(git_config_backend **out, git_config_backend *source); + +/** + * Create an in-memory configuration file backend + * + * @param out the new backend + * @param cfg the configuration that is to be parsed + * @param len the length of the string pointed to by `cfg` + */ +extern int git_config_backend_from_string(git_config_backend **out, const char *cfg, size_t len); + +GIT_INLINE(int) git_config_backend_open(git_config_backend *cfg, unsigned int level, const git_repository *repo) +{ + return cfg->open(cfg, level, repo); +} + +GIT_INLINE(void) git_config_backend_free(git_config_backend *cfg) +{ + if (cfg) + cfg->free(cfg); +} + +GIT_INLINE(int) git_config_backend_get_string( + git_config_entry **out, git_config_backend *cfg, const char *name) +{ + return cfg->get(cfg, name, out); +} + +GIT_INLINE(int) git_config_backend_set_string( + git_config_backend *cfg, const char *name, const char *value) +{ + return cfg->set(cfg, name, value); +} + +GIT_INLINE(int) git_config_backend_delete( + git_config_backend *cfg, const char *name) +{ + return cfg->del(cfg, name); +} + +GIT_INLINE(int) git_config_backend_foreach( + git_config_backend *cfg, + int (*fn)(const git_config_entry *entry, void *data), + void *data) +{ + return git_config_backend_foreach_match(cfg, NULL, fn, data); +} + +GIT_INLINE(int) git_config_backend_lock(git_config_backend *cfg) +{ + return cfg->lock(cfg); +} + +GIT_INLINE(int) git_config_backend_unlock(git_config_backend *cfg, int success) +{ + return cfg->unlock(cfg, success); +} + +#endif diff --git a/src/libgit2/config_cache.c b/src/libgit2/config_cache.c new file mode 100644 index 0000000..4bb91f5 --- /dev/null +++ b/src/libgit2/config_cache.c @@ -0,0 +1,142 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "futils.h" +#include "repository.h" +#include "config.h" +#include "git2/config.h" +#include "vector.h" +#include "filter.h" + +struct map_data { + const char *name; + git_configmap *maps; + size_t map_count; + int default_value; +}; + +/* + * core.eol + * Sets the line ending type to use in the working directory for + * files that have the text property set. Alternatives are lf, crlf + * and native, which uses the platform's native line ending. The default + * value is native. See gitattributes(5) for more information on + * end-of-line conversion. + */ +static git_configmap _configmap_eol[] = { + {GIT_CONFIGMAP_FALSE, NULL, GIT_EOL_UNSET}, + {GIT_CONFIGMAP_STRING, "lf", GIT_EOL_LF}, + {GIT_CONFIGMAP_STRING, "crlf", GIT_EOL_CRLF}, + {GIT_CONFIGMAP_STRING, "native", GIT_EOL_NATIVE} +}; + +/* + * core.autocrlf + * Setting this variable to "true" is almost the same as setting + * the text attribute to "auto" on all files except that text files are + * not guaranteed to be normalized: files that contain CRLF in the + * repository will not be touched. Use this setting if you want to have + * CRLF line endings in your working directory even though the repository + * does not have normalized line endings. This variable can be set to input, + * in which case no output conversion is performed. + */ +static git_configmap _configmap_autocrlf[] = { + {GIT_CONFIGMAP_FALSE, NULL, GIT_AUTO_CRLF_FALSE}, + {GIT_CONFIGMAP_TRUE, NULL, GIT_AUTO_CRLF_TRUE}, + {GIT_CONFIGMAP_STRING, "input", GIT_AUTO_CRLF_INPUT} +}; + +static git_configmap _configmap_safecrlf[] = { + {GIT_CONFIGMAP_FALSE, NULL, GIT_SAFE_CRLF_FALSE}, + {GIT_CONFIGMAP_TRUE, NULL, GIT_SAFE_CRLF_FAIL}, + {GIT_CONFIGMAP_STRING, "warn", GIT_SAFE_CRLF_WARN} +}; + +static git_configmap _configmap_logallrefupdates[] = { + {GIT_CONFIGMAP_FALSE, NULL, GIT_LOGALLREFUPDATES_FALSE}, + {GIT_CONFIGMAP_TRUE, NULL, GIT_LOGALLREFUPDATES_TRUE}, + {GIT_CONFIGMAP_STRING, "always", GIT_LOGALLREFUPDATES_ALWAYS}, +}; + +/* + * Generic map for integer values + */ +static git_configmap _configmap_int[] = { + {GIT_CONFIGMAP_INT32, NULL, 0}, +}; + +static struct map_data _configmaps[] = { + {"core.autocrlf", _configmap_autocrlf, ARRAY_SIZE(_configmap_autocrlf), GIT_AUTO_CRLF_DEFAULT}, + {"core.eol", _configmap_eol, ARRAY_SIZE(_configmap_eol), GIT_EOL_DEFAULT}, + {"core.symlinks", NULL, 0, GIT_SYMLINKS_DEFAULT }, + {"core.ignorecase", NULL, 0, GIT_IGNORECASE_DEFAULT }, + {"core.filemode", NULL, 0, GIT_FILEMODE_DEFAULT }, + {"core.ignorestat", NULL, 0, GIT_IGNORESTAT_DEFAULT }, + {"core.trustctime", NULL, 0, GIT_TRUSTCTIME_DEFAULT }, + {"core.abbrev", _configmap_int, 1, GIT_ABBREV_DEFAULT }, + {"core.precomposeunicode", NULL, 0, GIT_PRECOMPOSE_DEFAULT }, + {"core.safecrlf", _configmap_safecrlf, ARRAY_SIZE(_configmap_safecrlf), GIT_SAFE_CRLF_DEFAULT}, + {"core.logallrefupdates", _configmap_logallrefupdates, ARRAY_SIZE(_configmap_logallrefupdates), GIT_LOGALLREFUPDATES_DEFAULT}, + {"core.protecthfs", NULL, 0, GIT_PROTECTHFS_DEFAULT }, + {"core.protectntfs", NULL, 0, GIT_PROTECTNTFS_DEFAULT }, + {"core.fsyncobjectfiles", NULL, 0, GIT_FSYNCOBJECTFILES_DEFAULT }, + {"core.longpaths", NULL, 0, GIT_LONGPATHS_DEFAULT }, +}; + +int git_config__configmap_lookup(int *out, git_config *config, git_configmap_item item) +{ + int error = 0; + struct map_data *data = &_configmaps[(int)item]; + git_config_entry *entry; + + if ((error = git_config__lookup_entry(&entry, config, data->name, false)) < 0) + return error; + + if (!entry) + *out = data->default_value; + else if (data->maps) + error = git_config_lookup_map_value( + out, data->maps, data->map_count, entry->value); + else + error = git_config_parse_bool(out, entry->value); + + git_config_entry_free(entry); + return error; +} + +int git_repository__configmap_lookup(int *out, git_repository *repo, git_configmap_item item) +{ + intptr_t value = (intptr_t)git_atomic_load(repo->configmap_cache[(int)item]); + + *out = (int)value; + + if (value == GIT_CONFIGMAP_NOT_CACHED) { + git_config *config; + intptr_t oldval = value; + int error; + + if ((error = git_repository_config__weakptr(&config, repo)) < 0 || + (error = git_config__configmap_lookup(out, config, item)) < 0) + return error; + + value = *out; + git_atomic_compare_and_swap(&repo->configmap_cache[(int)item], (void *)oldval, (void *)value); + } + + return 0; +} + +void git_repository__configmap_lookup_cache_clear(git_repository *repo) +{ + int i; + + for (i = 0; i < GIT_CONFIGMAP_CACHE_MAX; ++i) + repo->configmap_cache[i] = GIT_CONFIGMAP_NOT_CACHED; +} + diff --git a/src/libgit2/config_entries.c b/src/libgit2/config_entries.c new file mode 100644 index 0000000..66aae09 --- /dev/null +++ b/src/libgit2/config_entries.c @@ -0,0 +1,237 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "config_entries.h" + +typedef struct config_entry_list { + struct config_entry_list *next; + struct config_entry_list *last; + git_config_entry *entry; +} config_entry_list; + +typedef struct { + git_config_entry *entry; + bool multivar; +} config_entry_map_head; + +typedef struct config_entries_iterator { + git_config_iterator parent; + git_config_entries *entries; + config_entry_list *head; +} config_entries_iterator; + +struct git_config_entries { + git_refcount rc; + git_strmap *map; + config_entry_list *list; +}; + +int git_config_entries_new(git_config_entries **out) +{ + git_config_entries *entries; + int error; + + entries = git__calloc(1, sizeof(git_config_entries)); + GIT_ERROR_CHECK_ALLOC(entries); + GIT_REFCOUNT_INC(entries); + + if ((error = git_strmap_new(&entries->map)) < 0) + git__free(entries); + else + *out = entries; + + return error; +} + +int git_config_entries_dup_entry(git_config_entries *entries, const git_config_entry *entry) +{ + git_config_entry *duplicated; + int error; + + duplicated = git__calloc(1, sizeof(git_config_entry)); + GIT_ERROR_CHECK_ALLOC(duplicated); + + duplicated->name = git__strdup(entry->name); + GIT_ERROR_CHECK_ALLOC(duplicated->name); + + if (entry->value) { + duplicated->value = git__strdup(entry->value); + GIT_ERROR_CHECK_ALLOC(duplicated->value); + } + duplicated->level = entry->level; + duplicated->include_depth = entry->include_depth; + + if ((error = git_config_entries_append(entries, duplicated)) < 0) + goto out; + +out: + if (error && duplicated) { + git__free((char *) duplicated->name); + git__free((char *) duplicated->value); + git__free(duplicated); + } + return error; +} + +int git_config_entries_dup(git_config_entries **out, git_config_entries *entries) +{ + git_config_entries *result = NULL; + config_entry_list *head; + int error; + + if ((error = git_config_entries_new(&result)) < 0) + goto out; + + for (head = entries->list; head; head = head->next) + if ((git_config_entries_dup_entry(result, head->entry)) < 0) + goto out; + + *out = result; + result = NULL; + +out: + git_config_entries_free(result); + return error; +} + +void git_config_entries_incref(git_config_entries *entries) +{ + GIT_REFCOUNT_INC(entries); +} + +static void config_entries_free(git_config_entries *entries) +{ + config_entry_list *list = NULL, *next; + config_entry_map_head *head; + + git_strmap_foreach_value(entries->map, head, + git__free((char *) head->entry->name); git__free(head) + ); + git_strmap_free(entries->map); + + list = entries->list; + while (list != NULL) { + next = list->next; + git__free((char *) list->entry->value); + git__free(list->entry); + git__free(list); + list = next; + } + + git__free(entries); +} + +void git_config_entries_free(git_config_entries *entries) +{ + if (entries) + GIT_REFCOUNT_DEC(entries, config_entries_free); +} + +int git_config_entries_append(git_config_entries *entries, git_config_entry *entry) +{ + config_entry_list *list_head; + config_entry_map_head *map_head; + + if ((map_head = git_strmap_get(entries->map, entry->name)) != NULL) { + map_head->multivar = true; + /* + * This is a micro-optimization for configuration files + * with a lot of same keys. As for multivars the entry's + * key will be the same for all entries, we can just free + * all except the first entry's name and just re-use it. + */ + git__free((char *) entry->name); + entry->name = map_head->entry->name; + } else { + map_head = git__calloc(1, sizeof(*map_head)); + if ((git_strmap_set(entries->map, entry->name, map_head)) < 0) + return -1; + } + map_head->entry = entry; + + list_head = git__calloc(1, sizeof(config_entry_list)); + GIT_ERROR_CHECK_ALLOC(list_head); + list_head->entry = entry; + + if (entries->list) + entries->list->last->next = list_head; + else + entries->list = list_head; + entries->list->last = list_head; + + return 0; +} + +int git_config_entries_get(git_config_entry **out, git_config_entries *entries, const char *key) +{ + config_entry_map_head *entry; + if ((entry = git_strmap_get(entries->map, key)) == NULL) + return GIT_ENOTFOUND; + *out = entry->entry; + return 0; +} + +int git_config_entries_get_unique(git_config_entry **out, git_config_entries *entries, const char *key) +{ + config_entry_map_head *entry; + + if ((entry = git_strmap_get(entries->map, key)) == NULL) + return GIT_ENOTFOUND; + + if (entry->multivar) { + git_error_set(GIT_ERROR_CONFIG, "entry is not unique due to being a multivar"); + return -1; + } + + if (entry->entry->include_depth) { + git_error_set(GIT_ERROR_CONFIG, "entry is not unique due to being included"); + return -1; + } + + *out = entry->entry; + + return 0; +} + +static void config_iterator_free(git_config_iterator *iter) +{ + config_entries_iterator *it = (config_entries_iterator *) iter; + git_config_entries_free(it->entries); + git__free(it); +} + +static int config_iterator_next( + git_config_entry **entry, + git_config_iterator *iter) +{ + config_entries_iterator *it = (config_entries_iterator *) iter; + + if (!it->head) + return GIT_ITEROVER; + + *entry = it->head->entry; + it->head = it->head->next; + + return 0; +} + +int git_config_entries_iterator_new(git_config_iterator **out, git_config_entries *entries) +{ + config_entries_iterator *it; + + it = git__calloc(1, sizeof(config_entries_iterator)); + GIT_ERROR_CHECK_ALLOC(it); + it->parent.next = config_iterator_next; + it->parent.free = config_iterator_free; + it->head = entries->list; + it->entries = entries; + + git_config_entries_incref(entries); + *out = &it->parent; + + return 0; +} diff --git a/src/libgit2/config_entries.h b/src/libgit2/config_entries.h new file mode 100644 index 0000000..832379e --- /dev/null +++ b/src/libgit2/config_entries.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "git2/sys/config.h" +#include "config.h" + +typedef struct git_config_entries git_config_entries; + +int git_config_entries_new(git_config_entries **out); +int git_config_entries_dup(git_config_entries **out, git_config_entries *entries); +int git_config_entries_dup_entry(git_config_entries *entries, const git_config_entry *entry); +void git_config_entries_incref(git_config_entries *entries); +void git_config_entries_free(git_config_entries *entries); +/* Add or append the new config option */ +int git_config_entries_append(git_config_entries *entries, git_config_entry *entry); +int git_config_entries_get(git_config_entry **out, git_config_entries *entries, const char *key); +int git_config_entries_get_unique(git_config_entry **out, git_config_entries *entries, const char *key); +int git_config_entries_iterator_new(git_config_iterator **out, git_config_entries *entries); diff --git a/src/libgit2/config_file.c b/src/libgit2/config_file.c new file mode 100644 index 0000000..716924d --- /dev/null +++ b/src/libgit2/config_file.c @@ -0,0 +1,1200 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "config.h" + +#include "git2/config.h" +#include "git2/sys/config.h" + +#include "array.h" +#include "str.h" +#include "config_backend.h" +#include "config_entries.h" +#include "config_parse.h" +#include "filebuf.h" +#include "regexp.h" +#include "sysdir.h" +#include "wildmatch.h" +#include "hash.h" + +/* Max depth for [include] directives */ +#define MAX_INCLUDE_DEPTH 10 + +typedef struct config_file { + git_futils_filestamp stamp; + unsigned char checksum[GIT_HASH_SHA256_SIZE]; + char *path; + git_array_t(struct config_file) includes; +} config_file; + +typedef struct { + git_config_backend parent; + git_mutex values_mutex; + git_config_entries *entries; + const git_repository *repo; + git_config_level_t level; + + git_array_t(git_config_parser) readers; + + bool locked; + git_filebuf locked_buf; + git_str locked_content; + + config_file file; +} config_file_backend; + +typedef struct { + const git_repository *repo; + config_file *file; + git_config_entries *entries; + git_config_level_t level; + unsigned int depth; +} config_file_parse_data; + +static int config_file_read(git_config_entries *entries, const git_repository *repo, config_file *file, git_config_level_t level, int depth); +static int config_file_read_buffer(git_config_entries *entries, const git_repository *repo, config_file *file, git_config_level_t level, int depth, const char *buf, size_t buflen); +static int config_file_write(config_file_backend *cfg, const char *orig_key, const char *key, const git_regexp *preg, const char *value); +static char *escape_value(const char *ptr); + +/** + * Take the current values map from the backend and increase its + * refcount. This is its own function to make sure we use the mutex to + * avoid the map pointer from changing under us. + */ +static int config_file_entries_take(git_config_entries **out, config_file_backend *b) +{ + int error; + + if ((error = git_mutex_lock(&b->values_mutex)) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock config backend"); + return error; + } + + git_config_entries_incref(b->entries); + *out = b->entries; + + git_mutex_unlock(&b->values_mutex); + + return 0; +} + +static void config_file_clear(config_file *file) +{ + config_file *include; + uint32_t i; + + if (file == NULL) + return; + + git_array_foreach(file->includes, i, include) { + config_file_clear(include); + } + git_array_clear(file->includes); + + git__free(file->path); +} + +static int config_file_open(git_config_backend *cfg, git_config_level_t level, const git_repository *repo) +{ + config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent); + int res; + + b->level = level; + b->repo = repo; + + if ((res = git_config_entries_new(&b->entries)) < 0) + return res; + + if (!git_fs_path_exists(b->file.path)) + return 0; + + /* + * git silently ignores configuration files that are not + * readable. We emulate that behavior. This is particularly + * important for sandboxed applications on macOS where the + * git configuration files may not be readable. + */ + if (p_access(b->file.path, R_OK) < 0) + return GIT_ENOTFOUND; + + if (res < 0 || (res = config_file_read(b->entries, repo, &b->file, level, 0)) < 0) { + git_config_entries_free(b->entries); + b->entries = NULL; + } + + return res; +} + +static int config_file_is_modified(int *modified, config_file *file) +{ + config_file *include; + git_str buf = GIT_STR_INIT; + unsigned char checksum[GIT_HASH_SHA256_SIZE]; + uint32_t i; + int error = 0; + + *modified = 0; + + if (!git_futils_filestamp_check(&file->stamp, file->path)) + goto check_includes; + + if ((error = git_futils_readbuffer(&buf, file->path)) < 0) + goto out; + + if ((error = git_hash_buf(checksum, buf.ptr, buf.size, GIT_HASH_ALGORITHM_SHA256)) < 0) + goto out; + + if (memcmp(checksum, file->checksum, GIT_HASH_SHA256_SIZE) != 0) { + *modified = 1; + goto out; + } + +check_includes: + git_array_foreach(file->includes, i, include) { + if ((error = config_file_is_modified(modified, include)) < 0 || *modified) + goto out; + } + +out: + git_str_dispose(&buf); + + return error; +} + +static void config_file_clear_includes(config_file_backend *cfg) +{ + config_file *include; + uint32_t i; + + git_array_foreach(cfg->file.includes, i, include) + config_file_clear(include); + git_array_clear(cfg->file.includes); +} + +static int config_file_set_entries(git_config_backend *cfg, git_config_entries *entries) +{ + config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent); + git_config_entries *old = NULL; + int error; + + if (b->parent.readonly) { + git_error_set(GIT_ERROR_CONFIG, "this backend is read-only"); + return -1; + } + + if ((error = git_mutex_lock(&b->values_mutex)) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock config backend"); + goto out; + } + + old = b->entries; + b->entries = entries; + + git_mutex_unlock(&b->values_mutex); + +out: + git_config_entries_free(old); + return error; +} + +static int config_file_refresh_from_buffer(git_config_backend *cfg, const char *buf, size_t buflen) +{ + config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent); + git_config_entries *entries = NULL; + int error; + + config_file_clear_includes(b); + + if ((error = git_config_entries_new(&entries)) < 0 || + (error = config_file_read_buffer(entries, b->repo, &b->file, + b->level, 0, buf, buflen)) < 0 || + (error = config_file_set_entries(cfg, entries)) < 0) + goto out; + + entries = NULL; +out: + git_config_entries_free(entries); + return error; +} + +static int config_file_refresh(git_config_backend *cfg) +{ + config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent); + git_config_entries *entries = NULL; + int error, modified; + + if (cfg->readonly) + return 0; + + if ((error = config_file_is_modified(&modified, &b->file)) < 0 && error != GIT_ENOTFOUND) + goto out; + + if (!modified) + return 0; + + config_file_clear_includes(b); + + if ((error = git_config_entries_new(&entries)) < 0 || + (error = config_file_read(entries, b->repo, &b->file, b->level, 0)) < 0 || + (error = config_file_set_entries(cfg, entries)) < 0) + goto out; + + entries = NULL; +out: + git_config_entries_free(entries); + + return (error == GIT_ENOTFOUND) ? 0 : error; +} + +static void config_file_free(git_config_backend *_backend) +{ + config_file_backend *backend = GIT_CONTAINER_OF(_backend, config_file_backend, parent); + + if (backend == NULL) + return; + + config_file_clear(&backend->file); + git_config_entries_free(backend->entries); + git_mutex_free(&backend->values_mutex); + git__free(backend); +} + +static int config_file_iterator( + git_config_iterator **iter, + struct git_config_backend *backend) +{ + config_file_backend *b = GIT_CONTAINER_OF(backend, config_file_backend, parent); + git_config_entries *dupped = NULL, *entries = NULL; + int error; + + if ((error = config_file_refresh(backend)) < 0 || + (error = config_file_entries_take(&entries, b)) < 0 || + (error = git_config_entries_dup(&dupped, entries)) < 0 || + (error = git_config_entries_iterator_new(iter, dupped)) < 0) + goto out; + +out: + /* Let iterator delete duplicated entries when it's done */ + git_config_entries_free(entries); + git_config_entries_free(dupped); + return error; +} + +static int config_file_snapshot(git_config_backend **out, git_config_backend *backend) +{ + return git_config_backend_snapshot(out, backend); +} + +static int config_file_set(git_config_backend *cfg, const char *name, const char *value) +{ + config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent); + git_config_entries *entries; + git_config_entry *existing; + char *key, *esc_value = NULL; + int error; + + if ((error = git_config__normalize_name(name, &key)) < 0) + return error; + + if ((error = config_file_entries_take(&entries, b)) < 0) + return error; + + /* Check whether we'd be modifying an included or multivar key */ + if ((error = git_config_entries_get_unique(&existing, entries, key)) < 0) { + if (error != GIT_ENOTFOUND) + goto out; + error = 0; + } else if ((!existing->value && !value) || + (existing->value && value && !strcmp(existing->value, value))) { + /* don't update if old and new values already match */ + error = 0; + goto out; + } + + /* No early returns due to sanity checks, let's write it out and refresh */ + if (value) { + esc_value = escape_value(value); + GIT_ERROR_CHECK_ALLOC(esc_value); + } + + if ((error = config_file_write(b, name, key, NULL, esc_value)) < 0) + goto out; + +out: + git_config_entries_free(entries); + git__free(esc_value); + git__free(key); + return error; +} + +/* release the map containing the entry as an equivalent to freeing it */ +static void config_file_entry_free(git_config_entry *entry) +{ + git_config_entries *entries = (git_config_entries *) entry->payload; + git_config_entries_free(entries); +} + +/* + * Internal function that actually gets the value in string form + */ +static int config_file_get(git_config_backend *cfg, const char *key, git_config_entry **out) +{ + config_file_backend *h = GIT_CONTAINER_OF(cfg, config_file_backend, parent); + git_config_entries *entries = NULL; + git_config_entry *entry; + int error = 0; + + if (!h->parent.readonly && ((error = config_file_refresh(cfg)) < 0)) + return error; + + if ((error = config_file_entries_take(&entries, h)) < 0) + return error; + + if ((error = (git_config_entries_get(&entry, entries, key))) < 0) { + git_config_entries_free(entries); + return error; + } + + entry->free = config_file_entry_free; + entry->payload = entries; + *out = entry; + + return 0; +} + +static int config_file_set_multivar( + git_config_backend *cfg, const char *name, const char *regexp, const char *value) +{ + config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent); + git_regexp preg; + int result; + char *key; + + GIT_ASSERT_ARG(regexp); + + if ((result = git_config__normalize_name(name, &key)) < 0) + return result; + + if ((result = git_regexp_compile(&preg, regexp, 0)) < 0) + goto out; + + /* If we do have it, set call config_file_write() and reload */ + if ((result = config_file_write(b, name, key, &preg, value)) < 0) + goto out; + +out: + git__free(key); + git_regexp_dispose(&preg); + + return result; +} + +static int config_file_delete(git_config_backend *cfg, const char *name) +{ + config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent); + git_config_entries *entries = NULL; + git_config_entry *entry; + char *key = NULL; + int error; + + if ((error = git_config__normalize_name(name, &key)) < 0) + goto out; + + if ((error = config_file_entries_take(&entries, b)) < 0) + goto out; + + /* Check whether we'd be modifying an included or multivar key */ + if ((error = git_config_entries_get_unique(&entry, entries, key)) < 0) { + if (error == GIT_ENOTFOUND) + git_error_set(GIT_ERROR_CONFIG, "could not find key '%s' to delete", name); + goto out; + } + + if ((error = config_file_write(b, name, entry->name, NULL, NULL)) < 0) + goto out; + +out: + git_config_entries_free(entries); + git__free(key); + return error; +} + +static int config_file_delete_multivar(git_config_backend *cfg, const char *name, const char *regexp) +{ + config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent); + git_config_entries *entries = NULL; + git_config_entry *entry = NULL; + git_regexp preg = GIT_REGEX_INIT; + char *key = NULL; + int result; + + if ((result = git_config__normalize_name(name, &key)) < 0) + goto out; + + if ((result = config_file_entries_take(&entries, b)) < 0) + goto out; + + if ((result = git_config_entries_get(&entry, entries, key)) < 0) { + if (result == GIT_ENOTFOUND) + git_error_set(GIT_ERROR_CONFIG, "could not find key '%s' to delete", name); + goto out; + } + + if ((result = git_regexp_compile(&preg, regexp, 0)) < 0) + goto out; + + if ((result = config_file_write(b, name, key, &preg, NULL)) < 0) + goto out; + +out: + git_config_entries_free(entries); + git__free(key); + git_regexp_dispose(&preg); + return result; +} + +static int config_file_lock(git_config_backend *_cfg) +{ + config_file_backend *cfg = GIT_CONTAINER_OF(_cfg, config_file_backend, parent); + int error; + + if ((error = git_filebuf_open(&cfg->locked_buf, cfg->file.path, 0, GIT_CONFIG_FILE_MODE)) < 0) + return error; + + error = git_futils_readbuffer(&cfg->locked_content, cfg->file.path); + if (error < 0 && error != GIT_ENOTFOUND) { + git_filebuf_cleanup(&cfg->locked_buf); + return error; + } + + cfg->locked = true; + return 0; + +} + +static int config_file_unlock(git_config_backend *_cfg, int success) +{ + config_file_backend *cfg = GIT_CONTAINER_OF(_cfg, config_file_backend, parent); + int error = 0; + + if (success) { + git_filebuf_write(&cfg->locked_buf, cfg->locked_content.ptr, cfg->locked_content.size); + error = git_filebuf_commit(&cfg->locked_buf); + } + + git_filebuf_cleanup(&cfg->locked_buf); + git_str_dispose(&cfg->locked_content); + cfg->locked = false; + + return error; +} + +int git_config_backend_from_file(git_config_backend **out, const char *path) +{ + config_file_backend *backend; + + backend = git__calloc(1, sizeof(config_file_backend)); + GIT_ERROR_CHECK_ALLOC(backend); + + backend->parent.version = GIT_CONFIG_BACKEND_VERSION; + git_mutex_init(&backend->values_mutex); + + backend->file.path = git__strdup(path); + GIT_ERROR_CHECK_ALLOC(backend->file.path); + git_array_init(backend->file.includes); + + backend->parent.open = config_file_open; + backend->parent.get = config_file_get; + backend->parent.set = config_file_set; + backend->parent.set_multivar = config_file_set_multivar; + backend->parent.del = config_file_delete; + backend->parent.del_multivar = config_file_delete_multivar; + backend->parent.iterator = config_file_iterator; + backend->parent.snapshot = config_file_snapshot; + backend->parent.lock = config_file_lock; + backend->parent.unlock = config_file_unlock; + backend->parent.free = config_file_free; + + *out = (git_config_backend *)backend; + + return 0; +} + +static int included_path(git_str *out, const char *dir, const char *path) +{ + /* From the user's home */ + if (path[0] == '~' && path[1] == '/') + return git_sysdir_expand_homedir_file(out, &path[1]); + + return git_fs_path_join_unrooted(out, path, dir, NULL); +} + +/* Escape the values to write them to the file */ +static char *escape_value(const char *ptr) +{ + git_str buf; + size_t len; + const char *esc; + + GIT_ASSERT_ARG_WITH_RETVAL(ptr, NULL); + + len = strlen(ptr); + if (!len) + return git__calloc(1, sizeof(char)); + + if (git_str_init(&buf, len) < 0) + return NULL; + + while (*ptr != '\0') { + if ((esc = strchr(git_config_escaped, *ptr)) != NULL) { + git_str_putc(&buf, '\\'); + git_str_putc(&buf, git_config_escapes[esc - git_config_escaped]); + } else { + git_str_putc(&buf, *ptr); + } + ptr++; + } + + if (git_str_oom(&buf)) + return NULL; + + return git_str_detach(&buf); +} + +static int parse_include(config_file_parse_data *parse_data, const char *file) +{ + config_file *include; + git_str path = GIT_STR_INIT; + char *dir; + int result; + + if (!file) + return 0; + + if ((result = git_fs_path_dirname_r(&path, parse_data->file->path)) < 0) + return result; + + dir = git_str_detach(&path); + result = included_path(&path, dir, file); + git__free(dir); + + if (result < 0) + return result; + + include = git_array_alloc(parse_data->file->includes); + GIT_ERROR_CHECK_ALLOC(include); + memset(include, 0, sizeof(*include)); + git_array_init(include->includes); + include->path = git_str_detach(&path); + + result = config_file_read(parse_data->entries, parse_data->repo, include, + parse_data->level, parse_data->depth+1); + + if (result == GIT_ENOTFOUND) { + git_error_clear(); + result = 0; + } + + return result; +} + +static int do_match_gitdir( + int *matches, + const git_repository *repo, + const char *cfg_file, + const char *condition, + bool case_insensitive) +{ + git_str pattern = GIT_STR_INIT, gitdir = GIT_STR_INIT; + int error; + + if (condition[0] == '.' && git_fs_path_is_dirsep(condition[1])) { + git_fs_path_dirname_r(&pattern, cfg_file); + git_str_joinpath(&pattern, pattern.ptr, condition + 2); + } else if (condition[0] == '~' && git_fs_path_is_dirsep(condition[1])) + git_sysdir_expand_homedir_file(&pattern, condition + 1); + else if (!git_fs_path_is_absolute(condition)) + git_str_joinpath(&pattern, "**", condition); + else + git_str_sets(&pattern, condition); + + if (git_fs_path_is_dirsep(condition[strlen(condition) - 1])) + git_str_puts(&pattern, "**"); + + if (git_str_oom(&pattern)) { + error = -1; + goto out; + } + + if ((error = git_repository__item_path(&gitdir, repo, GIT_REPOSITORY_ITEM_GITDIR)) < 0) + goto out; + + if (git_fs_path_is_dirsep(gitdir.ptr[gitdir.size - 1])) + git_str_truncate(&gitdir, gitdir.size - 1); + + *matches = wildmatch(pattern.ptr, gitdir.ptr, + WM_PATHNAME | (case_insensitive ? WM_CASEFOLD : 0)) == WM_MATCH; +out: + git_str_dispose(&pattern); + git_str_dispose(&gitdir); + return error; +} + +static int conditional_match_gitdir( + int *matches, + const git_repository *repo, + const char *cfg_file, + const char *value) +{ + return do_match_gitdir(matches, repo, cfg_file, value, false); +} + +static int conditional_match_gitdir_i( + int *matches, + const git_repository *repo, + const char *cfg_file, + const char *value) +{ + return do_match_gitdir(matches, repo, cfg_file, value, true); +} + +static int conditional_match_onbranch( + int *matches, + const git_repository *repo, + const char *cfg_file, + const char *condition) +{ + git_str reference = GIT_STR_INIT, buf = GIT_STR_INIT; + int error; + + GIT_UNUSED(cfg_file); + + /* + * NOTE: you cannot use `git_repository_head` here. Looking up the + * HEAD reference will create the ODB, which causes us to read the + * repo's config for keys like core.precomposeUnicode. As we're + * just parsing the config right now, though, this would result in + * an endless recursion. + */ + + if ((error = git_str_joinpath(&buf, git_repository_path(repo), GIT_HEAD_FILE)) < 0 || + (error = git_futils_readbuffer(&reference, buf.ptr)) < 0) + goto out; + git_str_rtrim(&reference); + + if (git__strncmp(reference.ptr, GIT_SYMREF, strlen(GIT_SYMREF))) + goto out; + git_str_consume(&reference, reference.ptr + strlen(GIT_SYMREF)); + + if (git__strncmp(reference.ptr, GIT_REFS_HEADS_DIR, strlen(GIT_REFS_HEADS_DIR))) + goto out; + git_str_consume(&reference, reference.ptr + strlen(GIT_REFS_HEADS_DIR)); + + /* + * If the condition ends with a '/', then we should treat it as if + * it had '**' appended. + */ + if ((error = git_str_sets(&buf, condition)) < 0) + goto out; + if (git_fs_path_is_dirsep(condition[strlen(condition) - 1]) && + (error = git_str_puts(&buf, "**")) < 0) + goto out; + + *matches = wildmatch(buf.ptr, reference.ptr, WM_PATHNAME) == WM_MATCH; +out: + git_str_dispose(&reference); + git_str_dispose(&buf); + + return error; + +} + +static const struct { + const char *prefix; + int (*matches)(int *matches, const git_repository *repo, const char *cfg, const char *value); +} conditions[] = { + { "gitdir:", conditional_match_gitdir }, + { "gitdir/i:", conditional_match_gitdir_i }, + { "onbranch:", conditional_match_onbranch } +}; + +static int parse_conditional_include(config_file_parse_data *parse_data, const char *section, const char *file) +{ + char *condition; + size_t section_len, i; + int error = 0, matches; + + if (!parse_data->repo || !file) + return 0; + + section_len = strlen(section); + + /* + * We checked that the string starts with `includeIf.` and ends + * in `.path` to get here. Make sure it consists of more. + */ + if (section_len < CONST_STRLEN("includeIf.") + CONST_STRLEN(".path")) + return 0; + + condition = git__substrdup(section + CONST_STRLEN("includeIf."), + section_len - CONST_STRLEN("includeIf.") - CONST_STRLEN(".path")); + + GIT_ERROR_CHECK_ALLOC(condition); + + for (i = 0; i < ARRAY_SIZE(conditions); i++) { + if (git__prefixcmp(condition, conditions[i].prefix)) + continue; + + if ((error = conditions[i].matches(&matches, + parse_data->repo, + parse_data->file->path, + condition + strlen(conditions[i].prefix))) < 0) + break; + + if (matches) + error = parse_include(parse_data, file); + + break; + } + + git__free(condition); + return error; +} + +static int read_on_variable( + git_config_parser *reader, + const char *current_section, + const char *var_name, + const char *var_value, + const char *line, + size_t line_len, + void *data) +{ + config_file_parse_data *parse_data = (config_file_parse_data *)data; + git_str buf = GIT_STR_INIT; + git_config_entry *entry; + const char *c; + int result = 0; + + GIT_UNUSED(reader); + GIT_UNUSED(line); + GIT_UNUSED(line_len); + + if (current_section) { + /* TODO: Once warnings lang, we should likely warn + * here. Git appears to warn in most cases if it sees + * un-namespaced config options. + */ + git_str_puts(&buf, current_section); + git_str_putc(&buf, '.'); + } + + for (c = var_name; *c; c++) + git_str_putc(&buf, git__tolower(*c)); + + if (git_str_oom(&buf)) + return -1; + + entry = git__calloc(1, sizeof(git_config_entry)); + GIT_ERROR_CHECK_ALLOC(entry); + entry->name = git_str_detach(&buf); + entry->value = var_value ? git__strdup(var_value) : NULL; + entry->level = parse_data->level; + entry->include_depth = parse_data->depth; + + if ((result = git_config_entries_append(parse_data->entries, entry)) < 0) + return result; + + result = 0; + + /* Add or append the new config option */ + if (!git__strcmp(entry->name, "include.path")) + result = parse_include(parse_data, entry->value); + else if (!git__prefixcmp(entry->name, "includeif.") && + !git__suffixcmp(entry->name, ".path")) + result = parse_conditional_include(parse_data, entry->name, entry->value); + + return result; +} + +static int config_file_read_buffer( + git_config_entries *entries, + const git_repository *repo, + config_file *file, + git_config_level_t level, + int depth, + const char *buf, + size_t buflen) +{ + config_file_parse_data parse_data; + git_config_parser reader; + int error; + + if (depth >= MAX_INCLUDE_DEPTH) { + git_error_set(GIT_ERROR_CONFIG, "maximum config include depth reached"); + return -1; + } + + /* Initialize the reading position */ + reader.path = file->path; + git_parse_ctx_init(&reader.ctx, buf, buflen); + + /* If the file is empty, there's nothing for us to do */ + if (!reader.ctx.content || *reader.ctx.content == '\0') { + error = 0; + goto out; + } + + parse_data.repo = repo; + parse_data.file = file; + parse_data.entries = entries; + parse_data.level = level; + parse_data.depth = depth; + + error = git_config_parse(&reader, NULL, read_on_variable, NULL, NULL, &parse_data); + +out: + return error; +} + +static int config_file_read( + git_config_entries *entries, + const git_repository *repo, + config_file *file, + git_config_level_t level, + int depth) +{ + git_str contents = GIT_STR_INIT; + struct stat st; + int error; + + if (p_stat(file->path, &st) < 0) { + error = git_fs_path_set_error(errno, file->path, "stat"); + goto out; + } + + if ((error = git_futils_readbuffer(&contents, file->path)) < 0) + goto out; + + git_futils_filestamp_set_from_stat(&file->stamp, &st); + if ((error = git_hash_buf(file->checksum, contents.ptr, contents.size, GIT_HASH_ALGORITHM_SHA256)) < 0) + goto out; + + if ((error = config_file_read_buffer(entries, repo, file, level, depth, + contents.ptr, contents.size)) < 0) + goto out; + +out: + git_str_dispose(&contents); + return error; +} + +static int write_section(git_str *fbuf, const char *key) +{ + int result; + const char *dot; + git_str buf = GIT_STR_INIT; + + /* All of this just for [section "subsection"] */ + dot = strchr(key, '.'); + git_str_putc(&buf, '['); + if (dot == NULL) { + git_str_puts(&buf, key); + } else { + char *escaped; + git_str_put(&buf, key, dot - key); + escaped = escape_value(dot + 1); + GIT_ERROR_CHECK_ALLOC(escaped); + git_str_printf(&buf, " \"%s\"", escaped); + git__free(escaped); + } + git_str_puts(&buf, "]\n"); + + if (git_str_oom(&buf)) + return -1; + + result = git_str_put(fbuf, git_str_cstr(&buf), buf.size); + git_str_dispose(&buf); + + return result; +} + +static const char *quotes_for_value(const char *value) +{ + const char *ptr; + + if (value[0] == ' ' || value[0] == '\0') + return "\""; + + for (ptr = value; *ptr; ++ptr) { + if (*ptr == ';' || *ptr == '#') + return "\""; + } + + if (ptr[-1] == ' ') + return "\""; + + return ""; +} + +struct write_data { + git_str *buf; + git_str buffered_comment; + unsigned int in_section : 1, + preg_replaced : 1; + const char *orig_section; + const char *section; + const char *orig_name; + const char *name; + const git_regexp *preg; + const char *value; +}; + +static int write_line_to(git_str *buf, const char *line, size_t line_len) +{ + int result = git_str_put(buf, line, line_len); + + if (!result && line_len && line[line_len-1] != '\n') + result = git_str_printf(buf, "\n"); + + return result; +} + +static int write_line(struct write_data *write_data, const char *line, size_t line_len) +{ + return write_line_to(write_data->buf, line, line_len); +} + +static int write_value(struct write_data *write_data) +{ + const char *q; + int result; + + q = quotes_for_value(write_data->value); + result = git_str_printf(write_data->buf, + "\t%s = %s%s%s\n", write_data->orig_name, q, write_data->value, q); + + /* If we are updating a single name/value, we're done. Setting `value` + * to `NULL` will prevent us from trying to write it again later (in + * `write_on_section`) if we see the same section repeated. + */ + if (!write_data->preg) + write_data->value = NULL; + + return result; +} + +static int write_on_section( + git_config_parser *reader, + const char *current_section, + const char *line, + size_t line_len, + void *data) +{ + struct write_data *write_data = (struct write_data *)data; + int result = 0; + + GIT_UNUSED(reader); + + /* If we were previously in the correct section (but aren't anymore) + * and haven't written our value (for a simple name/value set, not + * a multivar), then append it to the end of the section before writing + * the new one. + */ + if (write_data->in_section && !write_data->preg && write_data->value) + result = write_value(write_data); + + write_data->in_section = strcmp(current_section, write_data->section) == 0; + + /* + * If there were comments just before this section, dump them as well. + */ + if (!result) { + result = git_str_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size); + git_str_clear(&write_data->buffered_comment); + } + + if (!result) + result = write_line(write_data, line, line_len); + + return result; +} + +static int write_on_variable( + git_config_parser *reader, + const char *current_section, + const char *var_name, + const char *var_value, + const char *line, + size_t line_len, + void *data) +{ + struct write_data *write_data = (struct write_data *)data; + bool has_matched = false; + int error; + + GIT_UNUSED(reader); + GIT_UNUSED(current_section); + + /* + * If there were comments just before this variable, let's dump them as well. + */ + if ((error = git_str_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size)) < 0) + return error; + + git_str_clear(&write_data->buffered_comment); + + /* See if we are to update this name/value pair; first examine name */ + if (write_data->in_section && + strcasecmp(write_data->name, var_name) == 0) + has_matched = true; + + /* If we have a regex to match the value, see if it matches */ + if (has_matched && write_data->preg != NULL) + has_matched = (git_regexp_match(write_data->preg, var_value) == 0); + + /* If this isn't the name/value we're looking for, simply dump the + * existing data back out and continue on. + */ + if (!has_matched) + return write_line(write_data, line, line_len); + + write_data->preg_replaced = 1; + + /* If value is NULL, we are deleting this value; write nothing. */ + if (!write_data->value) + return 0; + + return write_value(write_data); +} + +static int write_on_comment(git_config_parser *reader, const char *line, size_t line_len, void *data) +{ + struct write_data *write_data; + + GIT_UNUSED(reader); + + write_data = (struct write_data *)data; + return write_line_to(&write_data->buffered_comment, line, line_len); +} + +static int write_on_eof( + git_config_parser *reader, const char *current_section, void *data) +{ + struct write_data *write_data = (struct write_data *)data; + int result = 0; + + GIT_UNUSED(reader); + + /* + * If we've buffered comments when reaching EOF, make sure to dump them. + */ + if ((result = git_str_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size)) < 0) + return result; + + /* If we are at the EOF and have not written our value (again, for a + * simple name/value set, not a multivar) then we have never seen the + * section in question and should create a new section and write the + * value. + */ + if ((!write_data->preg || !write_data->preg_replaced) && write_data->value) { + /* write the section header unless we're already in it */ + if (!current_section || strcmp(current_section, write_data->section)) + result = write_section(write_data->buf, write_data->orig_section); + + if (!result) + result = write_value(write_data); + } + + return result; +} + +/* + * This is pretty much the parsing, except we write out anything we don't have + */ +static int config_file_write( + config_file_backend *cfg, + const char *orig_key, + const char *key, + const git_regexp *preg, + const char *value) + +{ + char *orig_section = NULL, *section = NULL, *orig_name, *name, *ldot; + git_str buf = GIT_STR_INIT, contents = GIT_STR_INIT; + git_config_parser parser = GIT_CONFIG_PARSER_INIT; + git_filebuf file = GIT_FILEBUF_INIT; + struct write_data write_data; + int error; + + memset(&write_data, 0, sizeof(write_data)); + + if (cfg->locked) { + error = git_str_puts(&contents, git_str_cstr(&cfg->locked_content) == NULL ? "" : git_str_cstr(&cfg->locked_content)); + } else { + if ((error = git_filebuf_open(&file, cfg->file.path, + GIT_FILEBUF_HASH_SHA256, + GIT_CONFIG_FILE_MODE)) < 0) + goto done; + + /* We need to read in our own config file */ + error = git_futils_readbuffer(&contents, cfg->file.path); + } + if (error < 0 && error != GIT_ENOTFOUND) + goto done; + + if ((git_config_parser_init(&parser, cfg->file.path, contents.ptr, contents.size)) < 0) + goto done; + + ldot = strrchr(key, '.'); + name = ldot + 1; + section = git__strndup(key, ldot - key); + GIT_ERROR_CHECK_ALLOC(section); + + ldot = strrchr(orig_key, '.'); + orig_name = ldot + 1; + orig_section = git__strndup(orig_key, ldot - orig_key); + GIT_ERROR_CHECK_ALLOC(orig_section); + + write_data.buf = &buf; + write_data.orig_section = orig_section; + write_data.section = section; + write_data.orig_name = orig_name; + write_data.name = name; + write_data.preg = preg; + write_data.value = value; + + if ((error = git_config_parse(&parser, write_on_section, write_on_variable, + write_on_comment, write_on_eof, &write_data)) < 0) + goto done; + + if (cfg->locked) { + size_t len = buf.asize; + /* Update our copy with the modified contents */ + git_str_dispose(&cfg->locked_content); + git_str_attach(&cfg->locked_content, git_str_detach(&buf), len); + } else { + git_filebuf_write(&file, git_str_cstr(&buf), git_str_len(&buf)); + + if ((error = git_filebuf_commit(&file)) < 0) + goto done; + + if ((error = config_file_refresh_from_buffer(&cfg->parent, buf.ptr, buf.size)) < 0) + goto done; + } + +done: + git__free(section); + git__free(orig_section); + git_str_dispose(&write_data.buffered_comment); + git_str_dispose(&buf); + git_str_dispose(&contents); + git_filebuf_cleanup(&file); + git_config_parser_dispose(&parser); + + return error; +} diff --git a/src/libgit2/config_mem.c b/src/libgit2/config_mem.c new file mode 100644 index 0000000..560229c --- /dev/null +++ b/src/libgit2/config_mem.c @@ -0,0 +1,220 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "config.h" + +#include "config_backend.h" +#include "config_parse.h" +#include "config_entries.h" + +typedef struct { + git_config_backend parent; + git_config_entries *entries; + git_str cfg; +} config_memory_backend; + +typedef struct { + git_config_entries *entries; + git_config_level_t level; +} config_memory_parse_data; + +static int config_error_readonly(void) +{ + git_error_set(GIT_ERROR_CONFIG, "this backend is read-only"); + return -1; +} + +static int read_variable_cb( + git_config_parser *reader, + const char *current_section, + const char *var_name, + const char *var_value, + const char *line, + size_t line_len, + void *payload) +{ + config_memory_parse_data *parse_data = (config_memory_parse_data *) payload; + git_str buf = GIT_STR_INIT; + git_config_entry *entry; + const char *c; + int result; + + GIT_UNUSED(reader); + GIT_UNUSED(line); + GIT_UNUSED(line_len); + + if (current_section) { + /* TODO: Once warnings land, we should likely warn + * here. Git appears to warn in most cases if it sees + * un-namespaced config options. + */ + git_str_puts(&buf, current_section); + git_str_putc(&buf, '.'); + } + + for (c = var_name; *c; c++) + git_str_putc(&buf, git__tolower(*c)); + + if (git_str_oom(&buf)) + return -1; + + entry = git__calloc(1, sizeof(git_config_entry)); + GIT_ERROR_CHECK_ALLOC(entry); + entry->name = git_str_detach(&buf); + entry->value = var_value ? git__strdup(var_value) : NULL; + entry->level = parse_data->level; + entry->include_depth = 0; + + if ((result = git_config_entries_append(parse_data->entries, entry)) < 0) + return result; + + return result; +} + +static int config_memory_open(git_config_backend *backend, git_config_level_t level, const git_repository *repo) +{ + config_memory_backend *memory_backend = (config_memory_backend *) backend; + git_config_parser parser = GIT_PARSE_CTX_INIT; + config_memory_parse_data parse_data; + int error; + + GIT_UNUSED(repo); + + if ((error = git_config_parser_init(&parser, "in-memory", memory_backend->cfg.ptr, + memory_backend->cfg.size)) < 0) + goto out; + parse_data.entries = memory_backend->entries; + parse_data.level = level; + + if ((error = git_config_parse(&parser, NULL, read_variable_cb, NULL, NULL, &parse_data)) < 0) + goto out; + +out: + git_config_parser_dispose(&parser); + return error; +} + +static int config_memory_get(git_config_backend *backend, const char *key, git_config_entry **out) +{ + config_memory_backend *memory_backend = (config_memory_backend *) backend; + return git_config_entries_get(out, memory_backend->entries, key); +} + +static int config_memory_iterator( + git_config_iterator **iter, + git_config_backend *backend) +{ + config_memory_backend *memory_backend = (config_memory_backend *) backend; + git_config_entries *entries; + int error; + + if ((error = git_config_entries_dup(&entries, memory_backend->entries)) < 0) + goto out; + + if ((error = git_config_entries_iterator_new(iter, entries)) < 0) + goto out; + +out: + /* Let iterator delete duplicated entries when it's done */ + git_config_entries_free(entries); + return error; +} + +static int config_memory_set(git_config_backend *backend, const char *name, const char *value) +{ + GIT_UNUSED(backend); + GIT_UNUSED(name); + GIT_UNUSED(value); + return config_error_readonly(); +} + +static int config_memory_set_multivar( + git_config_backend *backend, const char *name, const char *regexp, const char *value) +{ + GIT_UNUSED(backend); + GIT_UNUSED(name); + GIT_UNUSED(regexp); + GIT_UNUSED(value); + return config_error_readonly(); +} + +static int config_memory_delete(git_config_backend *backend, const char *name) +{ + GIT_UNUSED(backend); + GIT_UNUSED(name); + return config_error_readonly(); +} + +static int config_memory_delete_multivar(git_config_backend *backend, const char *name, const char *regexp) +{ + GIT_UNUSED(backend); + GIT_UNUSED(name); + GIT_UNUSED(regexp); + return config_error_readonly(); +} + +static int config_memory_lock(git_config_backend *backend) +{ + GIT_UNUSED(backend); + return config_error_readonly(); +} + +static int config_memory_unlock(git_config_backend *backend, int success) +{ + GIT_UNUSED(backend); + GIT_UNUSED(success); + return config_error_readonly(); +} + +static void config_memory_free(git_config_backend *_backend) +{ + config_memory_backend *backend = (config_memory_backend *)_backend; + + if (backend == NULL) + return; + + git_config_entries_free(backend->entries); + git_str_dispose(&backend->cfg); + git__free(backend); +} + +int git_config_backend_from_string(git_config_backend **out, const char *cfg, size_t len) +{ + config_memory_backend *backend; + + backend = git__calloc(1, sizeof(config_memory_backend)); + GIT_ERROR_CHECK_ALLOC(backend); + + if (git_config_entries_new(&backend->entries) < 0) { + git__free(backend); + return -1; + } + + if (git_str_set(&backend->cfg, cfg, len) < 0) { + git_config_entries_free(backend->entries); + git__free(backend); + return -1; + } + + backend->parent.version = GIT_CONFIG_BACKEND_VERSION; + backend->parent.readonly = 1; + backend->parent.open = config_memory_open; + backend->parent.get = config_memory_get; + backend->parent.set = config_memory_set; + backend->parent.set_multivar = config_memory_set_multivar; + backend->parent.del = config_memory_delete; + backend->parent.del_multivar = config_memory_delete_multivar; + backend->parent.iterator = config_memory_iterator; + backend->parent.lock = config_memory_lock; + backend->parent.unlock = config_memory_unlock; + backend->parent.snapshot = git_config_backend_snapshot; + backend->parent.free = config_memory_free; + + *out = (git_config_backend *)backend; + + return 0; +} diff --git a/src/libgit2/config_parse.c b/src/libgit2/config_parse.c new file mode 100644 index 0000000..0693136 --- /dev/null +++ b/src/libgit2/config_parse.c @@ -0,0 +1,580 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "config_parse.h" + +#include + +const char *git_config_escapes = "ntb\"\\"; +const char *git_config_escaped = "\n\t\b\"\\"; + +static void set_parse_error(git_config_parser *reader, int col, const char *error_str) +{ + if (col) + git_error_set(GIT_ERROR_CONFIG, + "failed to parse config file: %s (in %s:%"PRIuZ", column %d)", + error_str, reader->path, reader->ctx.line_num, col); + else + git_error_set(GIT_ERROR_CONFIG, + "failed to parse config file: %s (in %s:%"PRIuZ")", + error_str, reader->path, reader->ctx.line_num); +} + + +GIT_INLINE(int) config_keychar(int c) +{ + return isalnum(c) || c == '-'; +} + +static int strip_comments(char *line, int in_quotes) +{ + int quote_count = in_quotes, backslash_count = 0; + char *ptr; + + for (ptr = line; *ptr; ++ptr) { + if (ptr[0] == '"' && ((ptr > line && ptr[-1] != '\\') || ptr == line)) + quote_count++; + + if ((ptr[0] == ';' || ptr[0] == '#') && + (quote_count % 2) == 0 && + (backslash_count % 2) == 0) { + ptr[0] = '\0'; + break; + } + + if (ptr[0] == '\\') + backslash_count++; + else + backslash_count = 0; + } + + /* skip any space at the end */ + while (ptr > line && git__isspace(ptr[-1])) { + ptr--; + } + ptr[0] = '\0'; + + return quote_count; +} + + +static int parse_subsection_header(git_config_parser *reader, const char *line, size_t pos, const char *base_name, char **section_name) +{ + int c, rpos; + const char *first_quote, *last_quote; + const char *line_start = line; + git_str buf = GIT_STR_INIT; + size_t quoted_len, alloc_len, base_name_len = strlen(base_name); + + /* Skip any additional whitespace before our section name */ + while (git__isspace(line[pos])) + pos++; + + /* We should be at the first quotation mark. */ + if (line[pos] != '"') { + set_parse_error(reader, 0, "missing quotation marks in section header"); + goto end_error; + } + + first_quote = &line[pos]; + last_quote = strrchr(line, '"'); + quoted_len = last_quote - first_quote; + + if ((last_quote - line) > INT_MAX) { + set_parse_error(reader, 0, "invalid section header, line too long"); + goto end_error; + } + + if (quoted_len == 0) { + set_parse_error(reader, 0, "missing closing quotation mark in section header"); + goto end_error; + } + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, base_name_len, quoted_len); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, 2); + + if (git_str_grow(&buf, alloc_len) < 0 || + git_str_printf(&buf, "%s.", base_name) < 0) + goto end_error; + + rpos = 0; + + line = first_quote; + c = line[++rpos]; + + /* + * At the end of each iteration, whatever is stored in c will be + * added to the string. In case of error, jump to out + */ + do { + + switch (c) { + case 0: + set_parse_error(reader, 0, "unexpected end-of-line in section header"); + goto end_error; + + case '"': + goto end_parse; + + case '\\': + c = line[++rpos]; + + if (c == 0) { + set_parse_error(reader, rpos, "unexpected end-of-line in section header"); + goto end_error; + } + + default: + break; + } + + git_str_putc(&buf, (char)c); + c = line[++rpos]; + } while (line + rpos < last_quote); + +end_parse: + if (git_str_oom(&buf)) + goto end_error; + + if (line[rpos] != '"' || line[rpos + 1] != ']') { + set_parse_error(reader, rpos, "unexpected text after closing quotes"); + git_str_dispose(&buf); + return -1; + } + + *section_name = git_str_detach(&buf); + return (int)(&line[rpos + 2] - line_start); /* rpos is at the closing quote */ + +end_error: + git_str_dispose(&buf); + + return -1; +} + +static int parse_section_header(git_config_parser *reader, char **section_out) +{ + char *name, *name_end; + int name_length, c, pos; + int result; + char *line; + size_t line_len; + + git_parse_advance_ws(&reader->ctx); + line = git__strndup(reader->ctx.line, reader->ctx.line_len); + if (line == NULL) + return -1; + + /* find the end of the variable's name */ + name_end = strrchr(line, ']'); + if (name_end == NULL) { + git__free(line); + set_parse_error(reader, 0, "missing ']' in section header"); + return -1; + } + + GIT_ERROR_CHECK_ALLOC_ADD(&line_len, (size_t)(name_end - line), 1); + name = git__malloc(line_len); + GIT_ERROR_CHECK_ALLOC(name); + + name_length = 0; + pos = 0; + + /* Make sure we were given a section header */ + c = line[pos++]; + GIT_ASSERT(c == '['); + + c = line[pos++]; + + do { + if (git__isspace(c)){ + name[name_length] = '\0'; + result = parse_subsection_header(reader, line, pos, name, section_out); + git__free(line); + git__free(name); + return result; + } + + if (!config_keychar(c) && c != '.') { + set_parse_error(reader, pos, "unexpected character in header"); + goto fail_parse; + } + + name[name_length++] = (char)git__tolower(c); + + } while ((c = line[pos++]) != ']'); + + if (line[pos - 1] != ']') { + set_parse_error(reader, pos, "unexpected end of file"); + goto fail_parse; + } + + git__free(line); + + name[name_length] = 0; + *section_out = name; + + return pos; + +fail_parse: + git__free(line); + git__free(name); + return -1; +} + +static int skip_bom(git_parse_ctx *parser) +{ + git_str buf = GIT_STR_INIT_CONST(parser->content, parser->content_len); + git_str_bom_t bom; + int bom_offset = git_str_detect_bom(&bom, &buf); + + if (bom == GIT_STR_BOM_UTF8) + git_parse_advance_chars(parser, bom_offset); + + /* TODO: reference implementation is pretty stupid with BoM */ + + return 0; +} + +/* + (* basic types *) + digit = "0".."9" + integer = digit { digit } + alphabet = "a".."z" + "A" .. "Z" + + section_char = alphabet | "." | "-" + extension_char = (* any character except newline *) + any_char = (* any character *) + variable_char = "alphabet" | "-" + + + (* actual grammar *) + config = { section } + + section = header { definition } + + header = "[" section [subsection | subsection_ext] "]" + + subsection = "." section + subsection_ext = "\"" extension "\"" + + section = section_char { section_char } + extension = extension_char { extension_char } + + definition = variable_name ["=" variable_value] "\n" + + variable_name = variable_char { variable_char } + variable_value = string | boolean | integer + + string = quoted_string | plain_string + quoted_string = "\"" plain_string "\"" + plain_string = { any_char } + + boolean = boolean_true | boolean_false + boolean_true = "yes" | "1" | "true" | "on" + boolean_false = "no" | "0" | "false" | "off" +*/ + +/* '\"' -> '"' etc */ +static int unescape_line( + char **out, bool *is_multi, const char *ptr, int quote_count) +{ + char *str, *fixed, *esc; + size_t ptr_len = strlen(ptr), alloc_len; + + *is_multi = false; + + if (GIT_ADD_SIZET_OVERFLOW(&alloc_len, ptr_len, 1) || + (str = git__malloc(alloc_len)) == NULL) { + return -1; + } + + fixed = str; + + while (*ptr != '\0') { + if (*ptr == '"') { + quote_count++; + } else if (*ptr != '\\') { + *fixed++ = *ptr; + } else { + /* backslash, check the next char */ + ptr++; + /* if we're at the end, it's a multiline, so keep the backslash */ + if (*ptr == '\0') { + *is_multi = true; + goto done; + } + if ((esc = strchr(git_config_escapes, *ptr)) != NULL) { + *fixed++ = git_config_escaped[esc - git_config_escapes]; + } else { + git__free(str); + git_error_set(GIT_ERROR_CONFIG, "invalid escape at %s", ptr); + return -1; + } + } + ptr++; + } + +done: + *fixed = '\0'; + *out = str; + + return 0; +} + +static int parse_multiline_variable(git_config_parser *reader, git_str *value, int in_quotes, size_t *line_len) +{ + int quote_count; + bool multiline = true; + + while (multiline) { + char *line = NULL, *proc_line = NULL; + int error; + + /* Check that the next line exists */ + git_parse_advance_line(&reader->ctx); + line = git__strndup(reader->ctx.line, reader->ctx.line_len); + GIT_ERROR_CHECK_ALLOC(line); + if (GIT_ADD_SIZET_OVERFLOW(line_len, *line_len, reader->ctx.line_len)) { + error = -1; + goto out; + } + + /* + * We've reached the end of the file, there is no continuation. + * (this is not an error). + */ + if (line[0] == '\0') { + error = 0; + goto out; + } + + /* If it was just a comment, pretend it didn't exist */ + quote_count = strip_comments(line, in_quotes); + if (line[0] == '\0') + goto next; + + if ((error = unescape_line(&proc_line, &multiline, + line, in_quotes)) < 0) + goto out; + + /* Add this line to the multiline var */ + if ((error = git_str_puts(value, proc_line)) < 0) + goto out; + +next: + git__free(line); + git__free(proc_line); + in_quotes = quote_count; + continue; + +out: + git__free(line); + git__free(proc_line); + return error; + } + + return 0; +} + +GIT_INLINE(bool) is_namechar(char c) +{ + return isalnum(c) || c == '-'; +} + +static int parse_name( + char **name, const char **value, git_config_parser *reader, const char *line) +{ + const char *name_end = line, *value_start; + + *name = NULL; + *value = NULL; + + while (*name_end && is_namechar(*name_end)) + name_end++; + + if (line == name_end) { + set_parse_error(reader, 0, "invalid configuration key"); + return -1; + } + + value_start = name_end; + + while (*value_start && git__isspace(*value_start)) + value_start++; + + if (*value_start == '=') { + *value = value_start + 1; + } else if (*value_start) { + set_parse_error(reader, 0, "invalid configuration key"); + return -1; + } + + if ((*name = git__strndup(line, name_end - line)) == NULL) + return -1; + + return 0; +} + +static int parse_variable(git_config_parser *reader, char **var_name, char **var_value, size_t *line_len) +{ + const char *value_start = NULL; + char *line = NULL, *name = NULL, *value = NULL; + int quote_count, error; + bool multiline; + + *var_name = NULL; + *var_value = NULL; + + git_parse_advance_ws(&reader->ctx); + line = git__strndup(reader->ctx.line, reader->ctx.line_len); + GIT_ERROR_CHECK_ALLOC(line); + + quote_count = strip_comments(line, 0); + + if ((error = parse_name(&name, &value_start, reader, line)) < 0) + goto out; + + /* + * Now, let's try to parse the value + */ + if (value_start != NULL) { + while (git__isspace(value_start[0])) + value_start++; + + if ((error = unescape_line(&value, &multiline, value_start, 0)) < 0) + goto out; + + if (multiline) { + git_str multi_value = GIT_STR_INIT; + git_str_attach(&multi_value, value, 0); + value = NULL; + + if (parse_multiline_variable(reader, &multi_value, quote_count % 2, line_len) < 0 || + git_str_oom(&multi_value)) { + error = -1; + git_str_dispose(&multi_value); + goto out; + } + + value = git_str_detach(&multi_value); + } + } + + *var_name = name; + *var_value = value; + name = NULL; + value = NULL; + +out: + git__free(name); + git__free(value); + git__free(line); + return error; +} + +int git_config_parser_init(git_config_parser *out, const char *path, const char *data, size_t datalen) +{ + out->path = path; + return git_parse_ctx_init(&out->ctx, data, datalen); +} + +void git_config_parser_dispose(git_config_parser *parser) +{ + git_parse_ctx_clear(&parser->ctx); +} + +int git_config_parse( + git_config_parser *parser, + git_config_parser_section_cb on_section, + git_config_parser_variable_cb on_variable, + git_config_parser_comment_cb on_comment, + git_config_parser_eof_cb on_eof, + void *payload) +{ + git_parse_ctx *ctx; + char *current_section = NULL, *var_name = NULL, *var_value = NULL; + int result = 0; + + ctx = &parser->ctx; + + skip_bom(ctx); + + for (; ctx->remain_len > 0; git_parse_advance_line(ctx)) { + const char *line_start; + size_t line_len; + char c; + + restart: + line_start = ctx->line; + line_len = ctx->line_len; + + /* + * Get either first non-whitespace character or, if that does + * not exist, the first whitespace character. This is required + * to preserve whitespaces when writing back the file. + */ + if (git_parse_peek(&c, ctx, GIT_PARSE_PEEK_SKIP_WHITESPACE) < 0 && + git_parse_peek(&c, ctx, 0) < 0) + continue; + + switch (c) { + case '[': /* section header, new section begins */ + git__free(current_section); + current_section = NULL; + + result = parse_section_header(parser, ¤t_section); + if (result < 0) + break; + + git_parse_advance_chars(ctx, result); + + if (on_section) + result = on_section(parser, current_section, line_start, line_len, payload); + /* + * After we've parsed the section header we may not be + * done with the line. If there's still data in there, + * run the next loop with the rest of the current line + * instead of moving forward. + */ + + if (!git_parse_peek(&c, ctx, GIT_PARSE_PEEK_SKIP_WHITESPACE)) + goto restart; + + break; + + case '\n': /* comment or whitespace-only */ + case '\r': + case ' ': + case '\t': + case ';': + case '#': + if (on_comment) { + result = on_comment(parser, line_start, line_len, payload); + } + break; + + default: /* assume variable declaration */ + if ((result = parse_variable(parser, &var_name, &var_value, &line_len)) == 0 && on_variable) { + result = on_variable(parser, current_section, var_name, var_value, line_start, line_len, payload); + git__free(var_name); + git__free(var_value); + } + + break; + } + + if (result < 0) + goto out; + } + + if (on_eof) + result = on_eof(parser, current_section, payload); + +out: + git__free(current_section); + return result; +} diff --git a/src/libgit2/config_parse.h b/src/libgit2/config_parse.h new file mode 100644 index 0000000..b791d32 --- /dev/null +++ b/src/libgit2/config_parse.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_config_parse_h__ +#define INCLUDE_config_parse_h__ + +#include "common.h" + +#include "array.h" +#include "futils.h" +#include "oid.h" +#include "parse.h" + +extern const char *git_config_escapes; +extern const char *git_config_escaped; + +typedef struct { + const char *path; + git_parse_ctx ctx; +} git_config_parser; + +#define GIT_CONFIG_PARSER_INIT { NULL, GIT_PARSE_CTX_INIT } + +typedef int (*git_config_parser_section_cb)( + git_config_parser *parser, + const char *current_section, + const char *line, + size_t line_len, + void *payload); + +typedef int (*git_config_parser_variable_cb)( + git_config_parser *parser, + const char *current_section, + const char *var_name, + const char *var_value, + const char *line, + size_t line_len, + void *payload); + +typedef int (*git_config_parser_comment_cb)( + git_config_parser *parser, + const char *line, + size_t line_len, + void *payload); + +typedef int (*git_config_parser_eof_cb)( + git_config_parser *parser, + const char *current_section, + void *payload); + +int git_config_parser_init(git_config_parser *out, const char *path, const char *data, size_t datalen); +void git_config_parser_dispose(git_config_parser *parser); + +int git_config_parse( + git_config_parser *parser, + git_config_parser_section_cb on_section, + git_config_parser_variable_cb on_variable, + git_config_parser_comment_cb on_comment, + git_config_parser_eof_cb on_eof, + void *payload); + +#endif diff --git a/src/libgit2/config_snapshot.c b/src/libgit2/config_snapshot.c new file mode 100644 index 0000000..e295d2f --- /dev/null +++ b/src/libgit2/config_snapshot.c @@ -0,0 +1,207 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "config_backend.h" + +#include "config.h" +#include "config_entries.h" + +typedef struct { + git_config_backend parent; + git_mutex values_mutex; + git_config_entries *entries; + git_config_backend *source; +} config_snapshot_backend; + +static int config_error_readonly(void) +{ + git_error_set(GIT_ERROR_CONFIG, "this backend is read-only"); + return -1; +} + +static int config_snapshot_iterator( + git_config_iterator **iter, + struct git_config_backend *backend) +{ + config_snapshot_backend *b = GIT_CONTAINER_OF(backend, config_snapshot_backend, parent); + git_config_entries *entries = NULL; + int error; + + if ((error = git_config_entries_dup(&entries, b->entries)) < 0 || + (error = git_config_entries_iterator_new(iter, entries)) < 0) + goto out; + +out: + /* Let iterator delete duplicated entries when it's done */ + git_config_entries_free(entries); + return error; +} + +/* release the map containing the entry as an equivalent to freeing it */ +static void config_snapshot_entry_free(git_config_entry *entry) +{ + git_config_entries *entries = (git_config_entries *) entry->payload; + git_config_entries_free(entries); +} + +static int config_snapshot_get(git_config_backend *cfg, const char *key, git_config_entry **out) +{ + config_snapshot_backend *b = GIT_CONTAINER_OF(cfg, config_snapshot_backend, parent); + git_config_entries *entries = NULL; + git_config_entry *entry; + int error = 0; + + if (git_mutex_lock(&b->values_mutex) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock config backend"); + return -1; + } + + entries = b->entries; + git_config_entries_incref(entries); + git_mutex_unlock(&b->values_mutex); + + if ((error = (git_config_entries_get(&entry, entries, key))) < 0) { + git_config_entries_free(entries); + return error; + } + + entry->free = config_snapshot_entry_free; + entry->payload = entries; + *out = entry; + + return 0; +} + +static int config_snapshot_set(git_config_backend *cfg, const char *name, const char *value) +{ + GIT_UNUSED(cfg); + GIT_UNUSED(name); + GIT_UNUSED(value); + + return config_error_readonly(); +} + +static int config_snapshot_set_multivar( + git_config_backend *cfg, const char *name, const char *regexp, const char *value) +{ + GIT_UNUSED(cfg); + GIT_UNUSED(name); + GIT_UNUSED(regexp); + GIT_UNUSED(value); + + return config_error_readonly(); +} + +static int config_snapshot_delete_multivar(git_config_backend *cfg, const char *name, const char *regexp) +{ + GIT_UNUSED(cfg); + GIT_UNUSED(name); + GIT_UNUSED(regexp); + + return config_error_readonly(); +} + +static int config_snapshot_delete(git_config_backend *cfg, const char *name) +{ + GIT_UNUSED(cfg); + GIT_UNUSED(name); + + return config_error_readonly(); +} + +static int config_snapshot_lock(git_config_backend *_cfg) +{ + GIT_UNUSED(_cfg); + + return config_error_readonly(); +} + +static int config_snapshot_unlock(git_config_backend *_cfg, int success) +{ + GIT_UNUSED(_cfg); + GIT_UNUSED(success); + + return config_error_readonly(); +} + +static void config_snapshot_free(git_config_backend *_backend) +{ + config_snapshot_backend *backend = GIT_CONTAINER_OF(_backend, config_snapshot_backend, parent); + + if (backend == NULL) + return; + + git_config_entries_free(backend->entries); + git_mutex_free(&backend->values_mutex); + git__free(backend); +} + +static int config_snapshot_open(git_config_backend *cfg, git_config_level_t level, const git_repository *repo) +{ + config_snapshot_backend *b = GIT_CONTAINER_OF(cfg, config_snapshot_backend, parent); + git_config_entries *entries = NULL; + git_config_iterator *it = NULL; + git_config_entry *entry; + int error; + + /* We're just copying data, don't care about the level or repo*/ + GIT_UNUSED(level); + GIT_UNUSED(repo); + + if ((error = git_config_entries_new(&entries)) < 0 || + (error = b->source->iterator(&it, b->source)) < 0) + goto out; + + while ((error = git_config_next(&entry, it)) == 0) + if ((error = git_config_entries_dup_entry(entries, entry)) < 0) + goto out; + + if (error < 0) { + if (error != GIT_ITEROVER) + goto out; + error = 0; + } + + b->entries = entries; + +out: + git_config_iterator_free(it); + if (error) + git_config_entries_free(entries); + return error; +} + +int git_config_backend_snapshot(git_config_backend **out, git_config_backend *source) +{ + config_snapshot_backend *backend; + + backend = git__calloc(1, sizeof(config_snapshot_backend)); + GIT_ERROR_CHECK_ALLOC(backend); + + backend->parent.version = GIT_CONFIG_BACKEND_VERSION; + git_mutex_init(&backend->values_mutex); + + backend->source = source; + + backend->parent.readonly = 1; + backend->parent.version = GIT_CONFIG_BACKEND_VERSION; + backend->parent.open = config_snapshot_open; + backend->parent.get = config_snapshot_get; + backend->parent.set = config_snapshot_set; + backend->parent.set_multivar = config_snapshot_set_multivar; + backend->parent.snapshot = git_config_backend_snapshot; + backend->parent.del = config_snapshot_delete; + backend->parent.del_multivar = config_snapshot_delete_multivar; + backend->parent.iterator = config_snapshot_iterator; + backend->parent.lock = config_snapshot_lock; + backend->parent.unlock = config_snapshot_unlock; + backend->parent.free = config_snapshot_free; + + *out = &backend->parent; + + return 0; +} diff --git a/src/libgit2/crlf.c b/src/libgit2/crlf.c new file mode 100644 index 0000000..1e1f1e8 --- /dev/null +++ b/src/libgit2/crlf.c @@ -0,0 +1,426 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "git2/attr.h" +#include "git2/blob.h" +#include "git2/index.h" +#include "git2/sys/filter.h" + +#include "buf.h" +#include "futils.h" +#include "hash.h" +#include "filter.h" +#include "repository.h" + +typedef enum { + GIT_CRLF_UNDEFINED, + GIT_CRLF_BINARY, + GIT_CRLF_TEXT, + GIT_CRLF_TEXT_INPUT, + GIT_CRLF_TEXT_CRLF, + GIT_CRLF_AUTO, + GIT_CRLF_AUTO_INPUT, + GIT_CRLF_AUTO_CRLF +} git_crlf_t; + +struct crlf_attrs { + int attr_action; /* the .gitattributes setting */ + int crlf_action; /* the core.autocrlf setting */ + + int auto_crlf; + int safe_crlf; + int core_eol; +}; + +struct crlf_filter { + git_filter f; +}; + +static git_crlf_t check_crlf(const char *value) +{ + if (GIT_ATTR_IS_TRUE(value)) + return GIT_CRLF_TEXT; + else if (GIT_ATTR_IS_FALSE(value)) + return GIT_CRLF_BINARY; + else if (GIT_ATTR_IS_UNSPECIFIED(value)) + ; + else if (strcmp(value, "input") == 0) + return GIT_CRLF_TEXT_INPUT; + else if (strcmp(value, "auto") == 0) + return GIT_CRLF_AUTO; + + return GIT_CRLF_UNDEFINED; +} + +static git_configmap_value check_eol(const char *value) +{ + if (GIT_ATTR_IS_UNSPECIFIED(value)) + ; + else if (strcmp(value, "lf") == 0) + return GIT_EOL_LF; + else if (strcmp(value, "crlf") == 0) + return GIT_EOL_CRLF; + + return GIT_EOL_UNSET; +} + +static int has_cr_in_index(const git_filter_source *src) +{ + git_repository *repo = git_filter_source_repo(src); + const char *path = git_filter_source_path(src); + git_index *index; + const git_index_entry *entry; + git_blob *blob; + const void *blobcontent; + git_object_size_t blobsize; + bool found_cr; + + if (!path) + return false; + + if (git_repository_index__weakptr(&index, repo) < 0) { + git_error_clear(); + return false; + } + + if (!(entry = git_index_get_bypath(index, path, 0)) && + !(entry = git_index_get_bypath(index, path, 1))) + return false; + + if (!S_ISREG(entry->mode)) /* don't crlf filter non-blobs */ + return true; + + if (git_blob_lookup(&blob, repo, &entry->id) < 0) + return false; + + blobcontent = git_blob_rawcontent(blob); + blobsize = git_blob_rawsize(blob); + if (!git__is_sizet(blobsize)) + blobsize = (size_t)-1; + + found_cr = (blobcontent != NULL && + blobsize > 0 && + memchr(blobcontent, '\r', (size_t)blobsize) != NULL); + + git_blob_free(blob); + return found_cr; +} + +static int text_eol_is_crlf(struct crlf_attrs *ca) +{ + if (ca->auto_crlf == GIT_AUTO_CRLF_TRUE) + return 1; + else if (ca->auto_crlf == GIT_AUTO_CRLF_INPUT) + return 0; + + if (ca->core_eol == GIT_EOL_CRLF) + return 1; + if (ca->core_eol == GIT_EOL_UNSET && GIT_EOL_NATIVE == GIT_EOL_CRLF) + return 1; + + return 0; +} + +static git_configmap_value output_eol(struct crlf_attrs *ca) +{ + switch (ca->crlf_action) { + case GIT_CRLF_BINARY: + return GIT_EOL_UNSET; + case GIT_CRLF_TEXT_CRLF: + return GIT_EOL_CRLF; + case GIT_CRLF_TEXT_INPUT: + return GIT_EOL_LF; + case GIT_CRLF_UNDEFINED: + case GIT_CRLF_AUTO_CRLF: + return GIT_EOL_CRLF; + case GIT_CRLF_AUTO_INPUT: + return GIT_EOL_LF; + case GIT_CRLF_TEXT: + case GIT_CRLF_AUTO: + return text_eol_is_crlf(ca) ? GIT_EOL_CRLF : GIT_EOL_LF; + } + + /* TODO: warn when available */ + return ca->core_eol; +} + +GIT_INLINE(int) check_safecrlf( + struct crlf_attrs *ca, + const git_filter_source *src, + git_str_text_stats *stats) +{ + const char *filename = git_filter_source_path(src); + + if (!ca->safe_crlf) + return 0; + + if (output_eol(ca) == GIT_EOL_LF) { + /* + * CRLFs would not be restored by checkout: + * check if we'd remove CRLFs + */ + if (stats->crlf) { + if (ca->safe_crlf == GIT_SAFE_CRLF_WARN) { + /* TODO: issue a warning when available */ + } else { + if (filename && *filename) + git_error_set( + GIT_ERROR_FILTER, "CRLF would be replaced by LF in '%s'", + filename); + else + git_error_set( + GIT_ERROR_FILTER, "CRLF would be replaced by LF"); + + return -1; + } + } + } else if (output_eol(ca) == GIT_EOL_CRLF) { + /* + * CRLFs would be added by checkout: + * check if we have "naked" LFs + */ + if (stats->crlf != stats->lf) { + if (ca->safe_crlf == GIT_SAFE_CRLF_WARN) { + /* TODO: issue a warning when available */ + } else { + if (filename && *filename) + git_error_set( + GIT_ERROR_FILTER, "LF would be replaced by CRLF in '%s'", + filename); + else + git_error_set( + GIT_ERROR_FILTER, "LF would be replaced by CRLF"); + + return -1; + } + } + } + + return 0; +} + +static int crlf_apply_to_odb( + struct crlf_attrs *ca, + git_str *to, + const git_str *from, + const git_filter_source *src) +{ + git_str_text_stats stats; + bool is_binary; + int error; + + /* Binary attribute? Empty file? Nothing to do */ + if (ca->crlf_action == GIT_CRLF_BINARY || from->size == 0) + return GIT_PASSTHROUGH; + + is_binary = git_str_gather_text_stats(&stats, from, false); + + /* Heuristics to see if we can skip the conversion. + * Straight from Core Git. + */ + if (ca->crlf_action == GIT_CRLF_AUTO || + ca->crlf_action == GIT_CRLF_AUTO_INPUT || + ca->crlf_action == GIT_CRLF_AUTO_CRLF) { + + if (is_binary) + return GIT_PASSTHROUGH; + + /* + * If the file in the index has any CR in it, do not convert. + * This is the new safer autocrlf handling. + */ + if (has_cr_in_index(src)) + return GIT_PASSTHROUGH; + } + + if ((error = check_safecrlf(ca, src, &stats)) < 0) + return error; + + /* If there are no CR characters to filter out, then just pass */ + if (!stats.crlf) + return GIT_PASSTHROUGH; + + /* Actually drop the carriage returns */ + return git_str_crlf_to_lf(to, from); +} + +static int crlf_apply_to_workdir( + struct crlf_attrs *ca, + git_str *to, + const git_str *from) +{ + git_str_text_stats stats; + bool is_binary; + + /* Empty file? Nothing to do. */ + if (git_str_len(from) == 0 || output_eol(ca) != GIT_EOL_CRLF) + return GIT_PASSTHROUGH; + + is_binary = git_str_gather_text_stats(&stats, from, false); + + /* If there are no LFs, or all LFs are part of a CRLF, nothing to do */ + if (stats.lf == 0 || stats.lf == stats.crlf) + return GIT_PASSTHROUGH; + + if (ca->crlf_action == GIT_CRLF_AUTO || + ca->crlf_action == GIT_CRLF_AUTO_INPUT || + ca->crlf_action == GIT_CRLF_AUTO_CRLF) { + + /* If we have any existing CR or CRLF line endings, do nothing */ + if (stats.cr > 0) + return GIT_PASSTHROUGH; + + /* Don't filter binary files */ + if (is_binary) + return GIT_PASSTHROUGH; + } + + return git_str_lf_to_crlf(to, from); +} + +static int convert_attrs( + struct crlf_attrs *ca, + const char **attr_values, + const git_filter_source *src) +{ + int error; + + memset(ca, 0, sizeof(struct crlf_attrs)); + + if ((error = git_repository__configmap_lookup(&ca->auto_crlf, + git_filter_source_repo(src), GIT_CONFIGMAP_AUTO_CRLF)) < 0 || + (error = git_repository__configmap_lookup(&ca->safe_crlf, + git_filter_source_repo(src), GIT_CONFIGMAP_SAFE_CRLF)) < 0 || + (error = git_repository__configmap_lookup(&ca->core_eol, + git_filter_source_repo(src), GIT_CONFIGMAP_EOL)) < 0) + return error; + + /* downgrade FAIL to WARN if ALLOW_UNSAFE option is used */ + if ((git_filter_source_flags(src) & GIT_FILTER_ALLOW_UNSAFE) && + ca->safe_crlf == GIT_SAFE_CRLF_FAIL) + ca->safe_crlf = GIT_SAFE_CRLF_WARN; + + if (attr_values) { + /* load the text attribute */ + ca->crlf_action = check_crlf(attr_values[2]); /* text */ + + if (ca->crlf_action == GIT_CRLF_UNDEFINED) + ca->crlf_action = check_crlf(attr_values[0]); /* crlf */ + + if (ca->crlf_action != GIT_CRLF_BINARY) { + /* load the eol attribute */ + int eol_attr = check_eol(attr_values[1]); + + if (ca->crlf_action == GIT_CRLF_AUTO && eol_attr == GIT_EOL_LF) + ca->crlf_action = GIT_CRLF_AUTO_INPUT; + else if (ca->crlf_action == GIT_CRLF_AUTO && eol_attr == GIT_EOL_CRLF) + ca->crlf_action = GIT_CRLF_AUTO_CRLF; + else if (eol_attr == GIT_EOL_LF) + ca->crlf_action = GIT_CRLF_TEXT_INPUT; + else if (eol_attr == GIT_EOL_CRLF) + ca->crlf_action = GIT_CRLF_TEXT_CRLF; + } + + ca->attr_action = ca->crlf_action; + } else { + ca->crlf_action = GIT_CRLF_UNDEFINED; + } + + if (ca->crlf_action == GIT_CRLF_TEXT) + ca->crlf_action = text_eol_is_crlf(ca) ? GIT_CRLF_TEXT_CRLF : GIT_CRLF_TEXT_INPUT; + if (ca->crlf_action == GIT_CRLF_UNDEFINED && ca->auto_crlf == GIT_AUTO_CRLF_FALSE) + ca->crlf_action = GIT_CRLF_BINARY; + if (ca->crlf_action == GIT_CRLF_UNDEFINED && ca->auto_crlf == GIT_AUTO_CRLF_TRUE) + ca->crlf_action = GIT_CRLF_AUTO_CRLF; + if (ca->crlf_action == GIT_CRLF_UNDEFINED && ca->auto_crlf == GIT_AUTO_CRLF_INPUT) + ca->crlf_action = GIT_CRLF_AUTO_INPUT; + + return 0; +} + +static int crlf_check( + git_filter *self, + void **payload, /* points to NULL ptr on entry, may be set */ + const git_filter_source *src, + const char **attr_values) +{ + struct crlf_attrs ca; + + GIT_UNUSED(self); + + convert_attrs(&ca, attr_values, src); + + if (ca.crlf_action == GIT_CRLF_BINARY) + return GIT_PASSTHROUGH; + + *payload = git__malloc(sizeof(ca)); + GIT_ERROR_CHECK_ALLOC(*payload); + memcpy(*payload, &ca, sizeof(ca)); + + return 0; +} + +static int crlf_apply( + git_filter *self, + void **payload, /* may be read and/or set */ + git_str *to, + const git_str *from, + const git_filter_source *src) +{ + int error = 0; + + /* initialize payload in case `check` was bypassed */ + if (!*payload) { + if ((error = crlf_check(self, payload, src, NULL)) < 0) + return error; + } + + if (git_filter_source_mode(src) == GIT_FILTER_SMUDGE) + error = crlf_apply_to_workdir(*payload, to, from); + else + error = crlf_apply_to_odb(*payload, to, from, src); + + return error; +} + +static int crlf_stream( + git_writestream **out, + git_filter *self, + void **payload, + const git_filter_source *src, + git_writestream *next) +{ + return git_filter_buffered_stream_new(out, + self, crlf_apply, NULL, payload, src, next); +} + +static void crlf_cleanup( + git_filter *self, + void *payload) +{ + GIT_UNUSED(self); + git__free(payload); +} + +git_filter *git_crlf_filter_new(void) +{ + struct crlf_filter *f = git__calloc(1, sizeof(struct crlf_filter)); + if (f == NULL) + return NULL; + + f->f.version = GIT_FILTER_VERSION; + f->f.attributes = "crlf eol text"; + f->f.initialize = NULL; + f->f.shutdown = git_filter_free; + f->f.check = crlf_check; + f->f.stream = crlf_stream; + f->f.cleanup = crlf_cleanup; + + return (git_filter *)f; +} diff --git a/src/libgit2/delta.c b/src/libgit2/delta.c new file mode 100644 index 0000000..2d2c5fa --- /dev/null +++ b/src/libgit2/delta.c @@ -0,0 +1,628 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "delta.h" + +/* maximum hash entry list for the same hash bucket */ +#define HASH_LIMIT 64 + +#define RABIN_SHIFT 23 +#define RABIN_WINDOW 16 + +static const unsigned int T[256] = { + 0x00000000, 0xab59b4d1, 0x56b369a2, 0xfdeadd73, 0x063f6795, 0xad66d344, + 0x508c0e37, 0xfbd5bae6, 0x0c7ecf2a, 0xa7277bfb, 0x5acda688, 0xf1941259, + 0x0a41a8bf, 0xa1181c6e, 0x5cf2c11d, 0xf7ab75cc, 0x18fd9e54, 0xb3a42a85, + 0x4e4ef7f6, 0xe5174327, 0x1ec2f9c1, 0xb59b4d10, 0x48719063, 0xe32824b2, + 0x1483517e, 0xbfdae5af, 0x423038dc, 0xe9698c0d, 0x12bc36eb, 0xb9e5823a, + 0x440f5f49, 0xef56eb98, 0x31fb3ca8, 0x9aa28879, 0x6748550a, 0xcc11e1db, + 0x37c45b3d, 0x9c9defec, 0x6177329f, 0xca2e864e, 0x3d85f382, 0x96dc4753, + 0x6b369a20, 0xc06f2ef1, 0x3bba9417, 0x90e320c6, 0x6d09fdb5, 0xc6504964, + 0x2906a2fc, 0x825f162d, 0x7fb5cb5e, 0xd4ec7f8f, 0x2f39c569, 0x846071b8, + 0x798aaccb, 0xd2d3181a, 0x25786dd6, 0x8e21d907, 0x73cb0474, 0xd892b0a5, + 0x23470a43, 0x881ebe92, 0x75f463e1, 0xdeadd730, 0x63f67950, 0xc8afcd81, + 0x354510f2, 0x9e1ca423, 0x65c91ec5, 0xce90aa14, 0x337a7767, 0x9823c3b6, + 0x6f88b67a, 0xc4d102ab, 0x393bdfd8, 0x92626b09, 0x69b7d1ef, 0xc2ee653e, + 0x3f04b84d, 0x945d0c9c, 0x7b0be704, 0xd05253d5, 0x2db88ea6, 0x86e13a77, + 0x7d348091, 0xd66d3440, 0x2b87e933, 0x80de5de2, 0x7775282e, 0xdc2c9cff, + 0x21c6418c, 0x8a9ff55d, 0x714a4fbb, 0xda13fb6a, 0x27f92619, 0x8ca092c8, + 0x520d45f8, 0xf954f129, 0x04be2c5a, 0xafe7988b, 0x5432226d, 0xff6b96bc, + 0x02814bcf, 0xa9d8ff1e, 0x5e738ad2, 0xf52a3e03, 0x08c0e370, 0xa39957a1, + 0x584ced47, 0xf3155996, 0x0eff84e5, 0xa5a63034, 0x4af0dbac, 0xe1a96f7d, + 0x1c43b20e, 0xb71a06df, 0x4ccfbc39, 0xe79608e8, 0x1a7cd59b, 0xb125614a, + 0x468e1486, 0xedd7a057, 0x103d7d24, 0xbb64c9f5, 0x40b17313, 0xebe8c7c2, + 0x16021ab1, 0xbd5bae60, 0x6cb54671, 0xc7ecf2a0, 0x3a062fd3, 0x915f9b02, + 0x6a8a21e4, 0xc1d39535, 0x3c394846, 0x9760fc97, 0x60cb895b, 0xcb923d8a, + 0x3678e0f9, 0x9d215428, 0x66f4eece, 0xcdad5a1f, 0x3047876c, 0x9b1e33bd, + 0x7448d825, 0xdf116cf4, 0x22fbb187, 0x89a20556, 0x7277bfb0, 0xd92e0b61, + 0x24c4d612, 0x8f9d62c3, 0x7836170f, 0xd36fa3de, 0x2e857ead, 0x85dcca7c, + 0x7e09709a, 0xd550c44b, 0x28ba1938, 0x83e3ade9, 0x5d4e7ad9, 0xf617ce08, + 0x0bfd137b, 0xa0a4a7aa, 0x5b711d4c, 0xf028a99d, 0x0dc274ee, 0xa69bc03f, + 0x5130b5f3, 0xfa690122, 0x0783dc51, 0xacda6880, 0x570fd266, 0xfc5666b7, + 0x01bcbbc4, 0xaae50f15, 0x45b3e48d, 0xeeea505c, 0x13008d2f, 0xb85939fe, + 0x438c8318, 0xe8d537c9, 0x153feaba, 0xbe665e6b, 0x49cd2ba7, 0xe2949f76, + 0x1f7e4205, 0xb427f6d4, 0x4ff24c32, 0xe4abf8e3, 0x19412590, 0xb2189141, + 0x0f433f21, 0xa41a8bf0, 0x59f05683, 0xf2a9e252, 0x097c58b4, 0xa225ec65, + 0x5fcf3116, 0xf49685c7, 0x033df00b, 0xa86444da, 0x558e99a9, 0xfed72d78, + 0x0502979e, 0xae5b234f, 0x53b1fe3c, 0xf8e84aed, 0x17bea175, 0xbce715a4, + 0x410dc8d7, 0xea547c06, 0x1181c6e0, 0xbad87231, 0x4732af42, 0xec6b1b93, + 0x1bc06e5f, 0xb099da8e, 0x4d7307fd, 0xe62ab32c, 0x1dff09ca, 0xb6a6bd1b, + 0x4b4c6068, 0xe015d4b9, 0x3eb80389, 0x95e1b758, 0x680b6a2b, 0xc352defa, + 0x3887641c, 0x93ded0cd, 0x6e340dbe, 0xc56db96f, 0x32c6cca3, 0x999f7872, + 0x6475a501, 0xcf2c11d0, 0x34f9ab36, 0x9fa01fe7, 0x624ac294, 0xc9137645, + 0x26459ddd, 0x8d1c290c, 0x70f6f47f, 0xdbaf40ae, 0x207afa48, 0x8b234e99, + 0x76c993ea, 0xdd90273b, 0x2a3b52f7, 0x8162e626, 0x7c883b55, 0xd7d18f84, + 0x2c043562, 0x875d81b3, 0x7ab75cc0, 0xd1eee811 +}; + +static const unsigned int U[256] = { + 0x00000000, 0x7eb5200d, 0x5633f4cb, 0x2886d4c6, 0x073e5d47, 0x798b7d4a, + 0x510da98c, 0x2fb88981, 0x0e7cba8e, 0x70c99a83, 0x584f4e45, 0x26fa6e48, + 0x0942e7c9, 0x77f7c7c4, 0x5f711302, 0x21c4330f, 0x1cf9751c, 0x624c5511, + 0x4aca81d7, 0x347fa1da, 0x1bc7285b, 0x65720856, 0x4df4dc90, 0x3341fc9d, + 0x1285cf92, 0x6c30ef9f, 0x44b63b59, 0x3a031b54, 0x15bb92d5, 0x6b0eb2d8, + 0x4388661e, 0x3d3d4613, 0x39f2ea38, 0x4747ca35, 0x6fc11ef3, 0x11743efe, + 0x3eccb77f, 0x40799772, 0x68ff43b4, 0x164a63b9, 0x378e50b6, 0x493b70bb, + 0x61bda47d, 0x1f088470, 0x30b00df1, 0x4e052dfc, 0x6683f93a, 0x1836d937, + 0x250b9f24, 0x5bbebf29, 0x73386bef, 0x0d8d4be2, 0x2235c263, 0x5c80e26e, + 0x740636a8, 0x0ab316a5, 0x2b7725aa, 0x55c205a7, 0x7d44d161, 0x03f1f16c, + 0x2c4978ed, 0x52fc58e0, 0x7a7a8c26, 0x04cfac2b, 0x73e5d470, 0x0d50f47d, + 0x25d620bb, 0x5b6300b6, 0x74db8937, 0x0a6ea93a, 0x22e87dfc, 0x5c5d5df1, + 0x7d996efe, 0x032c4ef3, 0x2baa9a35, 0x551fba38, 0x7aa733b9, 0x041213b4, + 0x2c94c772, 0x5221e77f, 0x6f1ca16c, 0x11a98161, 0x392f55a7, 0x479a75aa, + 0x6822fc2b, 0x1697dc26, 0x3e1108e0, 0x40a428ed, 0x61601be2, 0x1fd53bef, + 0x3753ef29, 0x49e6cf24, 0x665e46a5, 0x18eb66a8, 0x306db26e, 0x4ed89263, + 0x4a173e48, 0x34a21e45, 0x1c24ca83, 0x6291ea8e, 0x4d29630f, 0x339c4302, + 0x1b1a97c4, 0x65afb7c9, 0x446b84c6, 0x3adea4cb, 0x1258700d, 0x6ced5000, + 0x4355d981, 0x3de0f98c, 0x15662d4a, 0x6bd30d47, 0x56ee4b54, 0x285b6b59, + 0x00ddbf9f, 0x7e689f92, 0x51d01613, 0x2f65361e, 0x07e3e2d8, 0x7956c2d5, + 0x5892f1da, 0x2627d1d7, 0x0ea10511, 0x7014251c, 0x5facac9d, 0x21198c90, + 0x099f5856, 0x772a785b, 0x4c921c31, 0x32273c3c, 0x1aa1e8fa, 0x6414c8f7, + 0x4bac4176, 0x3519617b, 0x1d9fb5bd, 0x632a95b0, 0x42eea6bf, 0x3c5b86b2, + 0x14dd5274, 0x6a687279, 0x45d0fbf8, 0x3b65dbf5, 0x13e30f33, 0x6d562f3e, + 0x506b692d, 0x2ede4920, 0x06589de6, 0x78edbdeb, 0x5755346a, 0x29e01467, + 0x0166c0a1, 0x7fd3e0ac, 0x5e17d3a3, 0x20a2f3ae, 0x08242768, 0x76910765, + 0x59298ee4, 0x279caee9, 0x0f1a7a2f, 0x71af5a22, 0x7560f609, 0x0bd5d604, + 0x235302c2, 0x5de622cf, 0x725eab4e, 0x0ceb8b43, 0x246d5f85, 0x5ad87f88, + 0x7b1c4c87, 0x05a96c8a, 0x2d2fb84c, 0x539a9841, 0x7c2211c0, 0x029731cd, + 0x2a11e50b, 0x54a4c506, 0x69998315, 0x172ca318, 0x3faa77de, 0x411f57d3, + 0x6ea7de52, 0x1012fe5f, 0x38942a99, 0x46210a94, 0x67e5399b, 0x19501996, + 0x31d6cd50, 0x4f63ed5d, 0x60db64dc, 0x1e6e44d1, 0x36e89017, 0x485db01a, + 0x3f77c841, 0x41c2e84c, 0x69443c8a, 0x17f11c87, 0x38499506, 0x46fcb50b, + 0x6e7a61cd, 0x10cf41c0, 0x310b72cf, 0x4fbe52c2, 0x67388604, 0x198da609, + 0x36352f88, 0x48800f85, 0x6006db43, 0x1eb3fb4e, 0x238ebd5d, 0x5d3b9d50, + 0x75bd4996, 0x0b08699b, 0x24b0e01a, 0x5a05c017, 0x728314d1, 0x0c3634dc, + 0x2df207d3, 0x534727de, 0x7bc1f318, 0x0574d315, 0x2acc5a94, 0x54797a99, + 0x7cffae5f, 0x024a8e52, 0x06852279, 0x78300274, 0x50b6d6b2, 0x2e03f6bf, + 0x01bb7f3e, 0x7f0e5f33, 0x57888bf5, 0x293dabf8, 0x08f998f7, 0x764cb8fa, + 0x5eca6c3c, 0x207f4c31, 0x0fc7c5b0, 0x7172e5bd, 0x59f4317b, 0x27411176, + 0x1a7c5765, 0x64c97768, 0x4c4fa3ae, 0x32fa83a3, 0x1d420a22, 0x63f72a2f, + 0x4b71fee9, 0x35c4dee4, 0x1400edeb, 0x6ab5cde6, 0x42331920, 0x3c86392d, + 0x133eb0ac, 0x6d8b90a1, 0x450d4467, 0x3bb8646a +}; + +struct index_entry { + const unsigned char *ptr; + unsigned int val; + struct index_entry *next; +}; + +struct git_delta_index { + unsigned long memsize; + const void *src_buf; + size_t src_size; + unsigned int hash_mask; + struct index_entry *hash[GIT_FLEX_ARRAY]; +}; + +static int lookup_index_alloc( + void **out, unsigned long *out_len, size_t entries, size_t hash_count) +{ + size_t entries_len, hash_len, index_len; + + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&entries_len, entries, sizeof(struct index_entry)); + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&hash_len, hash_count, sizeof(struct index_entry *)); + + GIT_ERROR_CHECK_ALLOC_ADD(&index_len, sizeof(struct git_delta_index), entries_len); + GIT_ERROR_CHECK_ALLOC_ADD(&index_len, index_len, hash_len); + + if (!git__is_ulong(index_len)) { + git_error_set(GIT_ERROR_NOMEMORY, "overly large delta"); + return -1; + } + + *out = git__malloc(index_len); + GIT_ERROR_CHECK_ALLOC(*out); + + *out_len = (unsigned long)index_len; + return 0; +} + +int git_delta_index_init( + git_delta_index **out, const void *buf, size_t bufsize) +{ + unsigned int i, hsize, hmask, entries, prev_val, *hash_count; + const unsigned char *data, *buffer = buf; + struct git_delta_index *index; + struct index_entry *entry, **hash; + void *mem; + unsigned long memsize; + + *out = NULL; + + if (!buf || !bufsize) + return 0; + + /* Determine index hash size. Note that indexing skips the + first byte to allow for optimizing the rabin polynomial + initialization in create_delta(). */ + entries = (unsigned int)(bufsize - 1) / RABIN_WINDOW; + if (bufsize >= 0xffffffffUL) { + /* + * Current delta format can't encode offsets into + * reference buffer with more than 32 bits. + */ + entries = 0xfffffffeU / RABIN_WINDOW; + } + hsize = entries / 4; + for (i = 4; i < 31 && (1u << i) < hsize; i++); + hsize = 1 << i; + hmask = hsize - 1; + + if (lookup_index_alloc(&mem, &memsize, entries, hsize) < 0) + return -1; + + index = mem; + mem = index->hash; + hash = mem; + mem = hash + hsize; + entry = mem; + + index->memsize = memsize; + index->src_buf = buf; + index->src_size = bufsize; + index->hash_mask = hmask; + memset(hash, 0, hsize * sizeof(*hash)); + + /* allocate an array to count hash entries */ + hash_count = git__calloc(hsize, sizeof(*hash_count)); + if (!hash_count) { + git__free(index); + return -1; + } + + /* then populate the index */ + prev_val = ~0; + for (data = buffer + entries * RABIN_WINDOW - RABIN_WINDOW; + data >= buffer; + data -= RABIN_WINDOW) { + unsigned int val = 0; + for (i = 1; i <= RABIN_WINDOW; i++) + val = ((val << 8) | data[i]) ^ T[val >> RABIN_SHIFT]; + if (val == prev_val) { + /* keep the lowest of consecutive identical blocks */ + entry[-1].ptr = data + RABIN_WINDOW; + } else { + prev_val = val; + i = val & hmask; + entry->ptr = data + RABIN_WINDOW; + entry->val = val; + entry->next = hash[i]; + hash[i] = entry++; + hash_count[i]++; + } + } + + /* + * Determine a limit on the number of entries in the same hash + * bucket. This guard us against patological data sets causing + * really bad hash distribution with most entries in the same hash + * bucket that would bring us to O(m*n) computing costs (m and n + * corresponding to reference and target buffer sizes). + * + * Make sure none of the hash buckets has more entries than + * we're willing to test. Otherwise we cull the entry list + * uniformly to still preserve a good repartition across + * the reference buffer. + */ + for (i = 0; i < hsize; i++) { + if (hash_count[i] < HASH_LIMIT) + continue; + + entry = hash[i]; + do { + struct index_entry *keep = entry; + int skip = hash_count[i] / HASH_LIMIT / 2; + do { + entry = entry->next; + } while(--skip && entry); + keep->next = entry; + } while (entry); + } + git__free(hash_count); + + *out = index; + return 0; +} + +void git_delta_index_free(git_delta_index *index) +{ + git__free(index); +} + +size_t git_delta_index_size(git_delta_index *index) +{ + GIT_ASSERT_ARG(index); + + return index->memsize; +} + +/* + * The maximum size for any opcode sequence, including the initial header + * plus rabin window plus biggest copy. + */ +#define MAX_OP_SIZE (5 + 5 + 1 + RABIN_WINDOW + 7) + +int git_delta_create_from_index( + void **out, + size_t *out_len, + const struct git_delta_index *index, + const void *trg_buf, + size_t trg_size, + size_t max_size) +{ + unsigned int i, bufpos, bufsize, moff, msize, val; + int inscnt; + const unsigned char *ref_data, *ref_top, *data, *top; + unsigned char *buf; + + *out = NULL; + *out_len = 0; + + if (!trg_buf || !trg_size) + return 0; + + if (index->src_size > UINT_MAX || + trg_size > UINT_MAX || + max_size > (UINT_MAX - MAX_OP_SIZE - 1)) { + git_error_set(GIT_ERROR_INVALID, "buffer sizes too large for delta processing"); + return -1; + } + + bufpos = 0; + bufsize = 8192; + if (max_size && bufsize >= max_size) + bufsize = (unsigned int)(max_size + MAX_OP_SIZE + 1); + buf = git__malloc(bufsize); + GIT_ERROR_CHECK_ALLOC(buf); + + /* store reference buffer size */ + i = (unsigned int)index->src_size; + while (i >= 0x80) { + buf[bufpos++] = i | 0x80; + i >>= 7; + } + buf[bufpos++] = i; + + /* store target buffer size */ + i = (unsigned int)trg_size; + while (i >= 0x80) { + buf[bufpos++] = i | 0x80; + i >>= 7; + } + buf[bufpos++] = i; + + ref_data = index->src_buf; + ref_top = ref_data + index->src_size; + data = trg_buf; + top = (const unsigned char *) trg_buf + trg_size; + + bufpos++; + val = 0; + for (i = 0; i < RABIN_WINDOW && data < top; i++, data++) { + buf[bufpos++] = *data; + val = ((val << 8) | *data) ^ T[val >> RABIN_SHIFT]; + } + inscnt = i; + + moff = 0; + msize = 0; + while (data < top) { + if (msize < 4096) { + struct index_entry *entry; + val ^= U[data[-RABIN_WINDOW]]; + val = ((val << 8) | *data) ^ T[val >> RABIN_SHIFT]; + i = val & index->hash_mask; + for (entry = index->hash[i]; entry; entry = entry->next) { + const unsigned char *ref = entry->ptr; + const unsigned char *src = data; + unsigned int ref_size = (unsigned int)(ref_top - ref); + if (entry->val != val) + continue; + if (ref_size > (unsigned int)(top - src)) + ref_size = (unsigned int)(top - src); + if (ref_size <= msize) + break; + while (ref_size-- && *src++ == *ref) + ref++; + if (msize < (unsigned int)(ref - entry->ptr)) { + /* this is our best match so far */ + msize = (unsigned int)(ref - entry->ptr); + moff = (unsigned int)(entry->ptr - ref_data); + if (msize >= 4096) /* good enough */ + break; + } + } + } + + if (msize < 4) { + if (!inscnt) + bufpos++; + buf[bufpos++] = *data++; + inscnt++; + if (inscnt == 0x7f) { + buf[bufpos - inscnt - 1] = inscnt; + inscnt = 0; + } + msize = 0; + } else { + unsigned int left; + unsigned char *op; + + if (inscnt) { + while (moff && ref_data[moff-1] == data[-1]) { + /* we can match one byte back */ + msize++; + moff--; + data--; + bufpos--; + if (--inscnt) + continue; + bufpos--; /* remove count slot */ + inscnt--; /* make it -1 */ + break; + } + buf[bufpos - inscnt - 1] = inscnt; + inscnt = 0; + } + + /* A copy op is currently limited to 64KB (pack v2) */ + left = (msize < 0x10000) ? 0 : (msize - 0x10000); + msize -= left; + + op = buf + bufpos++; + i = 0x80; + + if (moff & 0x000000ff) + buf[bufpos++] = moff >> 0, i |= 0x01; + if (moff & 0x0000ff00) + buf[bufpos++] = moff >> 8, i |= 0x02; + if (moff & 0x00ff0000) + buf[bufpos++] = moff >> 16, i |= 0x04; + if (moff & 0xff000000) + buf[bufpos++] = moff >> 24, i |= 0x08; + + if (msize & 0x00ff) + buf[bufpos++] = msize >> 0, i |= 0x10; + if (msize & 0xff00) + buf[bufpos++] = msize >> 8, i |= 0x20; + + *op = i; + + data += msize; + moff += msize; + msize = left; + + if (msize < 4096) { + int j; + val = 0; + for (j = -RABIN_WINDOW; j < 0; j++) + val = ((val << 8) | data[j]) + ^ T[val >> RABIN_SHIFT]; + } + } + + if (bufpos >= bufsize - MAX_OP_SIZE) { + void *tmp = buf; + bufsize = bufsize * 3 / 2; + if (max_size && bufsize >= max_size) + bufsize = (unsigned int)(max_size + MAX_OP_SIZE + 1); + if (max_size && bufpos > max_size) + break; + buf = git__realloc(buf, bufsize); + if (!buf) { + git__free(tmp); + return -1; + } + } + } + + if (inscnt) + buf[bufpos - inscnt - 1] = inscnt; + + if (max_size && bufpos > max_size) { + git_error_set(GIT_ERROR_NOMEMORY, "delta would be larger than maximum size"); + git__free(buf); + return GIT_EBUFS; + } + + *out_len = bufpos; + *out = buf; + return 0; +} + +/* +* Delta application was heavily cribbed from BinaryDelta.java in JGit, which +* itself was heavily cribbed from patch-delta.c in the +* GIT project. The original delta patching code was written by +* Nicolas Pitre . +*/ + +static int hdr_sz( + size_t *size, + const unsigned char **delta, + const unsigned char *end) +{ + const unsigned char *d = *delta; + size_t r = 0; + unsigned int c, shift = 0; + + do { + if (d == end) { + git_error_set(GIT_ERROR_INVALID, "truncated delta"); + return -1; + } + + c = *d++; + r |= (c & 0x7f) << shift; + shift += 7; + } while (c & 0x80); + *delta = d; + *size = r; + return 0; +} + +int git_delta_read_header( + size_t *base_out, + size_t *result_out, + const unsigned char *delta, + size_t delta_len) +{ + const unsigned char *delta_end = delta + delta_len; + if ((hdr_sz(base_out, &delta, delta_end) < 0) || + (hdr_sz(result_out, &delta, delta_end) < 0)) + return -1; + return 0; +} + +#define DELTA_HEADER_BUFFER_LEN 16 +int git_delta_read_header_fromstream( + size_t *base_sz, size_t *res_sz, git_packfile_stream *stream) +{ + static const size_t buffer_len = DELTA_HEADER_BUFFER_LEN; + unsigned char buffer[DELTA_HEADER_BUFFER_LEN]; + const unsigned char *delta, *delta_end; + size_t len; + ssize_t read; + + len = read = 0; + while (len < buffer_len) { + read = git_packfile_stream_read(stream, &buffer[len], buffer_len - len); + + if (read == 0) + break; + + if (read == GIT_EBUFS) + continue; + + len += read; + } + + delta = buffer; + delta_end = delta + len; + if ((hdr_sz(base_sz, &delta, delta_end) < 0) || + (hdr_sz(res_sz, &delta, delta_end) < 0)) + return -1; + + return 0; +} + +int git_delta_apply( + void **out, + size_t *out_len, + const unsigned char *base, + size_t base_len, + const unsigned char *delta, + size_t delta_len) +{ + const unsigned char *delta_end = delta + delta_len; + size_t base_sz, res_sz, alloc_sz; + unsigned char *res_dp; + + *out = NULL; + *out_len = 0; + + /* + * Check that the base size matches the data we were given; + * if not we would underflow while accessing data from the + * base object, resulting in data corruption or segfault. + */ + if ((hdr_sz(&base_sz, &delta, delta_end) < 0) || (base_sz != base_len)) { + git_error_set(GIT_ERROR_INVALID, "failed to apply delta: base size does not match given data"); + return -1; + } + + if (hdr_sz(&res_sz, &delta, delta_end) < 0) { + git_error_set(GIT_ERROR_INVALID, "failed to apply delta: base size does not match given data"); + return -1; + } + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_sz, res_sz, 1); + res_dp = git__malloc(alloc_sz); + GIT_ERROR_CHECK_ALLOC(res_dp); + + res_dp[res_sz] = '\0'; + *out = res_dp; + *out_len = res_sz; + + while (delta < delta_end) { + unsigned char cmd = *delta++; + if (cmd & 0x80) { + /* cmd is a copy instruction; copy from the base. */ + size_t off = 0, len = 0, end; + +#define ADD_DELTA(o, shift) { if (delta < delta_end) (o) |= ((unsigned) *delta++ << shift); else goto fail; } + if (cmd & 0x01) ADD_DELTA(off, 0UL); + if (cmd & 0x02) ADD_DELTA(off, 8UL); + if (cmd & 0x04) ADD_DELTA(off, 16UL); + if (cmd & 0x08) ADD_DELTA(off, 24UL); + + if (cmd & 0x10) ADD_DELTA(len, 0UL); + if (cmd & 0x20) ADD_DELTA(len, 8UL); + if (cmd & 0x40) ADD_DELTA(len, 16UL); + if (!len) len = 0x10000; +#undef ADD_DELTA + + if (GIT_ADD_SIZET_OVERFLOW(&end, off, len) || + base_len < end || res_sz < len) + goto fail; + + memcpy(res_dp, base + off, len); + res_dp += len; + res_sz -= len; + + } else if (cmd) { + /* + * cmd is a literal insert instruction; copy from + * the delta stream itself. + */ + if (delta_end - delta < cmd || res_sz < cmd) + goto fail; + memcpy(res_dp, delta, cmd); + delta += cmd; + res_dp += cmd; + res_sz -= cmd; + + } else { + /* cmd == 0 is reserved for future encodings. */ + goto fail; + } + } + + if (delta != delta_end || res_sz) + goto fail; + return 0; + +fail: + git__free(*out); + + *out = NULL; + *out_len = 0; + + git_error_set(GIT_ERROR_INVALID, "failed to apply delta"); + return -1; +} diff --git a/src/libgit2/delta.h b/src/libgit2/delta.h new file mode 100644 index 0000000..f619873 --- /dev/null +++ b/src/libgit2/delta.h @@ -0,0 +1,136 @@ +/* + * diff-delta code taken from git.git. See diff-delta.c for details. + * + */ +#ifndef INCLUDE_git_delta_h__ +#define INCLUDE_git_delta_h__ + +#include "common.h" + +#include "pack.h" + +typedef struct git_delta_index git_delta_index; + +/* + * git_delta_index_init: compute index data from given buffer + * + * This returns a pointer to a struct delta_index that should be passed to + * subsequent create_delta() calls, or to free_delta_index(). A NULL pointer + * is returned on failure. The given buffer must not be freed nor altered + * before free_delta_index() is called. The returned pointer must be freed + * using free_delta_index(). + */ +extern int git_delta_index_init( + git_delta_index **out, const void *buf, size_t bufsize); + +/* + * Free the index created by git_delta_index_init() + */ +extern void git_delta_index_free(git_delta_index *index); + +/* + * Returns memory usage of delta index. + */ +extern size_t git_delta_index_size(git_delta_index *index); + +/* + * create_delta: create a delta from given index for the given buffer + * + * This function may be called multiple times with different buffers using + * the same delta_index pointer. If max_delta_size is non-zero and the + * resulting delta is to be larger than max_delta_size then NULL is returned. + * On success, a non-NULL pointer to the buffer with the delta data is + * returned and *delta_size is updated with its size. The returned buffer + * must be freed by the caller. + */ +extern int git_delta_create_from_index( + void **out, + size_t *out_size, + const struct git_delta_index *index, + const void *buf, + size_t bufsize, + size_t max_delta_size); + +/* + * diff_delta: create a delta from source buffer to target buffer + * + * If max_delta_size is non-zero and the resulting delta is to be larger + * than max_delta_size then GIT_EBUFS is returned. On success, a non-NULL + * pointer to the buffer with the delta data is returned and *delta_size is + * updated with its size. The returned buffer must be freed by the caller. + */ +GIT_INLINE(int) git_delta( + void **out, size_t *out_len, + const void *src_buf, size_t src_bufsize, + const void *trg_buf, size_t trg_bufsize, + size_t max_delta_size) +{ + git_delta_index *index; + int error = 0; + + *out = NULL; + *out_len = 0; + + if ((error = git_delta_index_init(&index, src_buf, src_bufsize)) < 0) + return error; + + if (index) { + error = git_delta_create_from_index(out, out_len, + index, trg_buf, trg_bufsize, max_delta_size); + + git_delta_index_free(index); + } + + return error; +} + +/* the smallest possible delta size is 4 bytes */ +#define GIT_DELTA_SIZE_MIN 4 + +/** +* Apply a git binary delta to recover the original content. +* The caller is responsible for freeing the returned buffer. +* +* @param out the output buffer +* @param out_len the length of the output buffer +* @param base the base to copy from during copy instructions. +* @param base_len number of bytes available at base. +* @param delta the delta to execute copy/insert instructions from. +* @param delta_len total number of bytes in the delta. +* @return 0 on success or an error code +*/ +extern int git_delta_apply( + void **out, + size_t *out_len, + const unsigned char *base, + size_t base_len, + const unsigned char *delta, + size_t delta_len); + +/** +* Read the header of a git binary delta. +* +* @param base_out pointer to store the base size field. +* @param result_out pointer to store the result size field. +* @param delta the delta to execute copy/insert instructions from. +* @param delta_len total number of bytes in the delta. +* @return 0 on success or an error code +*/ +extern int git_delta_read_header( + size_t *base_out, + size_t *result_out, + const unsigned char *delta, + size_t delta_len); + +/** + * Read the header of a git binary delta + * + * This variant reads just enough from the packfile stream to read the + * delta header. + */ +extern int git_delta_read_header_fromstream( + size_t *base_out, + size_t *result_out, + git_packfile_stream *stream); + +#endif diff --git a/src/libgit2/describe.c b/src/libgit2/describe.c new file mode 100644 index 0000000..0445347 --- /dev/null +++ b/src/libgit2/describe.c @@ -0,0 +1,912 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "git2/describe.h" +#include "git2/diff.h" +#include "git2/status.h" + +#include "buf.h" +#include "commit.h" +#include "commit_list.h" +#include "oidmap.h" +#include "refs.h" +#include "repository.h" +#include "revwalk.h" +#include "strarray.h" +#include "tag.h" +#include "vector.h" +#include "wildmatch.h" + +/* Ported from https://github.com/git/git/blob/89dde7882f71f846ccd0359756d27bebc31108de/builtin/describe.c */ + +struct commit_name { + git_tag *tag; + unsigned prio:2; /* annotated tag = 2, tag = 1, head = 0 */ + unsigned name_checked:1; + git_oid sha1; + char *path; + + /* Khash workaround. They original key has to still be reachable */ + git_oid peeled; +}; + +static void *oidmap_value_bykey(git_oidmap *map, const git_oid *key) +{ + return git_oidmap_get(map, key); +} + +static struct commit_name *find_commit_name( + git_oidmap *names, + const git_oid *peeled) +{ + return (struct commit_name *)(oidmap_value_bykey(names, peeled)); +} + +static int replace_name( + git_tag **tag, + git_repository *repo, + struct commit_name *e, + unsigned int prio, + const git_oid *sha1) +{ + git_time_t e_time = 0, t_time = 0; + + if (!e || e->prio < prio) + return 1; + + if (e->prio == 2 && prio == 2) { + /* Multiple annotated tags point to the same commit. + * Select one to keep based upon their tagger date. + */ + git_tag *t = NULL; + + if (!e->tag) { + if (git_tag_lookup(&t, repo, &e->sha1) < 0) + return 1; + e->tag = t; + } + + if (git_tag_lookup(&t, repo, sha1) < 0) + return 0; + + *tag = t; + + if (e->tag->tagger) + e_time = e->tag->tagger->when.time; + + if (t->tagger) + t_time = t->tagger->when.time; + + if (e_time < t_time) + return 1; + } + + return 0; +} + +static int add_to_known_names( + git_repository *repo, + git_oidmap *names, + const char *path, + const git_oid *peeled, + unsigned int prio, + const git_oid *sha1) +{ + struct commit_name *e = find_commit_name(names, peeled); + bool found = (e != NULL); + + git_tag *tag = NULL; + if (replace_name(&tag, repo, e, prio, sha1)) { + if (!found) { + e = git__malloc(sizeof(struct commit_name)); + GIT_ERROR_CHECK_ALLOC(e); + + e->path = NULL; + e->tag = NULL; + } + + if (e->tag) + git_tag_free(e->tag); + e->tag = tag; + e->prio = prio; + e->name_checked = 0; + git_oid_cpy(&e->sha1, sha1); + git__free(e->path); + e->path = git__strdup(path); + git_oid_cpy(&e->peeled, peeled); + + if (!found && git_oidmap_set(names, &e->peeled, e) < 0) + return -1; + } + else + git_tag_free(tag); + + return 0; +} + +static int retrieve_peeled_tag_or_object_oid( + git_oid *peeled_out, + git_oid *ref_target_out, + git_repository *repo, + const char *refname) +{ + git_reference *ref; + git_object *peeled = NULL; + int error; + + if ((error = git_reference_lookup_resolved(&ref, repo, refname, -1)) < 0) + return error; + + if ((error = git_reference_peel(&peeled, ref, GIT_OBJECT_ANY)) < 0) + goto cleanup; + + git_oid_cpy(ref_target_out, git_reference_target(ref)); + git_oid_cpy(peeled_out, git_object_id(peeled)); + + if (git_oid_cmp(ref_target_out, peeled_out) != 0) + error = 1; /* The reference was pointing to a annotated tag */ + else + error = 0; /* Any other object */ + +cleanup: + git_reference_free(ref); + git_object_free(peeled); + return error; +} + +struct git_describe_result { + int dirty; + int exact_match; + int fallback_to_id; + git_oid commit_id; + git_repository *repo; + struct commit_name *name; + struct possible_tag *tag; +}; + +struct get_name_data +{ + git_describe_options *opts; + git_repository *repo; + git_oidmap *names; + git_describe_result *result; +}; + +static int commit_name_dup(struct commit_name **out, struct commit_name *in) +{ + struct commit_name *name; + + name = git__malloc(sizeof(struct commit_name)); + GIT_ERROR_CHECK_ALLOC(name); + + memcpy(name, in, sizeof(struct commit_name)); + name->tag = NULL; + name->path = NULL; + + if (in->tag && git_tag_dup(&name->tag, in->tag) < 0) + return -1; + + name->path = git__strdup(in->path); + GIT_ERROR_CHECK_ALLOC(name->path); + + *out = name; + return 0; +} + +static int get_name(const char *refname, void *payload) +{ + struct get_name_data *data; + bool is_tag, is_annotated, all; + git_oid peeled, sha1; + unsigned int prio; + int error = 0; + + data = (struct get_name_data *)payload; + is_tag = !git__prefixcmp(refname, GIT_REFS_TAGS_DIR); + all = data->opts->describe_strategy == GIT_DESCRIBE_ALL; + + /* Reject anything outside refs/tags/ unless --all */ + if (!all && !is_tag) + return 0; + + /* Accept only tags that match the pattern, if given */ + if (data->opts->pattern && (!is_tag || wildmatch(data->opts->pattern, + refname + strlen(GIT_REFS_TAGS_DIR), 0))) + return 0; + + /* Is it annotated? */ + if ((error = retrieve_peeled_tag_or_object_oid( + &peeled, &sha1, data->repo, refname)) < 0) + return error; + + is_annotated = error; + + /* + * By default, we only use annotated tags, but with --tags + * we fall back to lightweight ones (even without --tags, + * we still remember lightweight ones, only to give hints + * in an error message). --all allows any refs to be used. + */ + if (is_annotated) + prio = 2; + else if (is_tag) + prio = 1; + else + prio = 0; + + add_to_known_names(data->repo, data->names, + all ? refname + strlen(GIT_REFS_DIR) : refname + strlen(GIT_REFS_TAGS_DIR), + &peeled, prio, &sha1); + return 0; +} + +struct possible_tag { + struct commit_name *name; + int depth; + int found_order; + unsigned flag_within; +}; + +static int possible_tag_dup(struct possible_tag **out, struct possible_tag *in) +{ + struct possible_tag *tag; + int error; + + tag = git__malloc(sizeof(struct possible_tag)); + GIT_ERROR_CHECK_ALLOC(tag); + + memcpy(tag, in, sizeof(struct possible_tag)); + tag->name = NULL; + + if ((error = commit_name_dup(&tag->name, in->name)) < 0) { + git__free(tag); + *out = NULL; + return error; + } + + *out = tag; + return 0; +} + +static int compare_pt(const void *a_, const void *b_) +{ + struct possible_tag *a = (struct possible_tag *)a_; + struct possible_tag *b = (struct possible_tag *)b_; + if (a->depth != b->depth) + return a->depth - b->depth; + if (a->found_order != b->found_order) + return a->found_order - b->found_order; + return 0; +} + +#define SEEN (1u << 0) + +static unsigned long finish_depth_computation( + git_pqueue *list, + git_revwalk *walk, + struct possible_tag *best) +{ + unsigned long seen_commits = 0; + int error, i; + + while (git_pqueue_size(list) > 0) { + git_commit_list_node *c = git_pqueue_pop(list); + seen_commits++; + if (c->flags & best->flag_within) { + size_t index = 0; + while (git_pqueue_size(list) > index) { + git_commit_list_node *i = git_pqueue_get(list, index); + if (!(i->flags & best->flag_within)) + break; + index++; + } + if (index > git_pqueue_size(list)) + break; + } else + best->depth++; + for (i = 0; i < c->out_degree; i++) { + git_commit_list_node *p = c->parents[i]; + if ((error = git_commit_list_parse(walk, p)) < 0) + return error; + if (!(p->flags & SEEN)) + if ((error = git_pqueue_insert(list, p)) < 0) + return error; + p->flags |= c->flags; + } + } + return seen_commits; +} + +static int display_name(git_str *buf, git_repository *repo, struct commit_name *n) +{ + if (n->prio == 2 && !n->tag) { + if (git_tag_lookup(&n->tag, repo, &n->sha1) < 0) { + git_error_set(GIT_ERROR_TAG, "annotated tag '%s' not available", n->path); + return -1; + } + } + + if (n->tag && !n->name_checked) { + if (!git_tag_name(n->tag)) { + git_error_set(GIT_ERROR_TAG, "annotated tag '%s' has no embedded name", n->path); + return -1; + } + + /* TODO: Cope with warnings + if (strcmp(n->tag->tag, all ? n->path + 5 : n->path)) + warning(_("tag '%s' is really '%s' here"), n->tag->tag, n->path); + */ + + n->name_checked = 1; + } + + if (n->tag) + git_str_printf(buf, "%s", git_tag_name(n->tag)); + else + git_str_printf(buf, "%s", n->path); + + return 0; +} + +static int find_unique_abbrev_size( + int *out, + git_repository *repo, + const git_oid *oid_in, + unsigned int abbreviated_size) +{ + size_t size = abbreviated_size; + git_odb *odb; + git_oid dummy; + size_t hexsize; + int error; + + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0) + return error; + + hexsize = git_oid_hexsize(repo->oid_type); + + while (size < hexsize) { + if ((error = git_odb_exists_prefix(&dummy, odb, oid_in, size)) == 0) { + *out = (int) size; + return 0; + } + + /* If the error wasn't that it's not unique, then it's a proper error */ + if (error != GIT_EAMBIGUOUS) + return error; + + /* Try again with a larger size */ + size++; + } + + /* If we didn't find any shorter prefix, we have to do the whole thing */ + *out = (int)hexsize; + + return 0; +} + +static int show_suffix( + git_str *buf, + int depth, + git_repository *repo, + const git_oid *id, + unsigned int abbrev_size) +{ + int error, size = 0; + + char hex_oid[GIT_OID_MAX_HEXSIZE]; + + if ((error = find_unique_abbrev_size(&size, repo, id, abbrev_size)) < 0) + return error; + + git_oid_fmt(hex_oid, id); + + git_str_printf(buf, "-%d-g", depth); + + git_str_put(buf, hex_oid, size); + + return git_str_oom(buf) ? -1 : 0; +} + +#define MAX_CANDIDATES_TAGS FLAG_BITS - 1 + +static int describe_not_found(const git_oid *oid, const char *message_format) { + char oid_str[GIT_OID_MAX_HEXSIZE + 1]; + git_oid_tostr(oid_str, sizeof(oid_str), oid); + + git_error_set(GIT_ERROR_DESCRIBE, message_format, oid_str); + return GIT_ENOTFOUND; +} + +static int describe( + struct get_name_data *data, + git_commit *commit) +{ + struct commit_name *n; + struct possible_tag *best; + bool all, tags; + git_revwalk *walk = NULL; + git_pqueue list; + git_commit_list_node *cmit, *gave_up_on = NULL; + git_vector all_matches = GIT_VECTOR_INIT; + unsigned int match_cnt = 0, annotated_cnt = 0, cur_match; + unsigned long seen_commits = 0; /* TODO: Check long */ + unsigned int unannotated_cnt = 0; + int error; + + if (git_vector_init(&all_matches, MAX_CANDIDATES_TAGS, compare_pt) < 0) + return -1; + + if ((error = git_pqueue_init(&list, 0, 2, git_commit_list_time_cmp)) < 0) + goto cleanup; + + all = data->opts->describe_strategy == GIT_DESCRIBE_ALL; + tags = data->opts->describe_strategy == GIT_DESCRIBE_TAGS; + + git_oid_cpy(&data->result->commit_id, git_commit_id(commit)); + + n = find_commit_name(data->names, git_commit_id(commit)); + if (n && (tags || all || n->prio == 2)) { + /* + * Exact match to an existing ref. + */ + data->result->exact_match = 1; + if ((error = commit_name_dup(&data->result->name, n)) < 0) + goto cleanup; + + goto cleanup; + } + + if (!data->opts->max_candidates_tags) { + error = describe_not_found( + git_commit_id(commit), + "cannot describe - no tag exactly matches '%s'"); + + goto cleanup; + } + + if ((error = git_revwalk_new(&walk, git_commit_owner(commit))) < 0) + goto cleanup; + + if ((cmit = git_revwalk__commit_lookup(walk, git_commit_id(commit))) == NULL) + goto cleanup; + + if ((error = git_commit_list_parse(walk, cmit)) < 0) + goto cleanup; + + cmit->flags = SEEN; + + if ((error = git_pqueue_insert(&list, cmit)) < 0) + goto cleanup; + + while (git_pqueue_size(&list) > 0) + { + int i; + + git_commit_list_node *c = (git_commit_list_node *)git_pqueue_pop(&list); + seen_commits++; + + n = find_commit_name(data->names, &c->oid); + + if (n) { + if (!tags && !all && n->prio < 2) { + unannotated_cnt++; + } else if (match_cnt < data->opts->max_candidates_tags) { + struct possible_tag *t = git__malloc(sizeof(struct commit_name)); + GIT_ERROR_CHECK_ALLOC(t); + if ((error = git_vector_insert(&all_matches, t)) < 0) + goto cleanup; + + match_cnt++; + + t->name = n; + t->depth = seen_commits - 1; + t->flag_within = 1u << match_cnt; + t->found_order = match_cnt; + c->flags |= t->flag_within; + if (n->prio == 2) + annotated_cnt++; + } + else { + gave_up_on = c; + break; + } + } + + for (cur_match = 0; cur_match < match_cnt; cur_match++) { + struct possible_tag *t = git_vector_get(&all_matches, cur_match); + if (!(c->flags & t->flag_within)) + t->depth++; + } + + if (annotated_cnt && (git_pqueue_size(&list) == 0)) { + /* + if (debug) { + char oid_str[GIT_OID_MAX_HEXSIZE + 1]; + git_oid_tostr(oid_str, sizeof(oid_str), &c->oid); + + fprintf(stderr, "finished search at %s\n", oid_str); + } + */ + break; + } + for (i = 0; i < c->out_degree; i++) { + git_commit_list_node *p = c->parents[i]; + if ((error = git_commit_list_parse(walk, p)) < 0) + goto cleanup; + if (!(p->flags & SEEN)) + if ((error = git_pqueue_insert(&list, p)) < 0) + goto cleanup; + p->flags |= c->flags; + + if (data->opts->only_follow_first_parent) + break; + } + } + + if (!match_cnt) { + if (data->opts->show_commit_oid_as_fallback) { + data->result->fallback_to_id = 1; + git_oid_cpy(&data->result->commit_id, &cmit->oid); + + goto cleanup; + } + if (unannotated_cnt) { + error = describe_not_found(git_commit_id(commit), + "cannot describe - " + "no annotated tags can describe '%s'; " + "however, there were unannotated tags."); + goto cleanup; + } + else { + error = describe_not_found(git_commit_id(commit), + "cannot describe - " + "no tags can describe '%s'."); + goto cleanup; + } + } + + git_vector_sort(&all_matches); + + best = (struct possible_tag *)git_vector_get(&all_matches, 0); + + if (gave_up_on) { + if ((error = git_pqueue_insert(&list, gave_up_on)) < 0) + goto cleanup; + seen_commits--; + } + if ((error = finish_depth_computation( + &list, walk, best)) < 0) + goto cleanup; + + seen_commits += error; + if ((error = possible_tag_dup(&data->result->tag, best)) < 0) + goto cleanup; + + /* + { + static const char *prio_names[] = { + "head", "lightweight", "annotated", + }; + + char oid_str[GIT_OID_MAX_HEXSIZE + 1]; + + if (debug) { + for (cur_match = 0; cur_match < match_cnt; cur_match++) { + struct possible_tag *t = (struct possible_tag *)git_vector_get(&all_matches, cur_match); + fprintf(stderr, " %-11s %8d %s\n", + prio_names[t->name->prio], + t->depth, t->name->path); + } + fprintf(stderr, "traversed %lu commits\n", seen_commits); + if (gave_up_on) { + git_oid_tostr(oid_str, sizeof(oid_str), &gave_up_on->oid); + fprintf(stderr, + "more than %i tags found; listed %i most recent\n" + "gave up search at %s\n", + data->opts->max_candidates_tags, data->opts->max_candidates_tags, + oid_str); + } + } + } + */ + + git_oid_cpy(&data->result->commit_id, &cmit->oid); + +cleanup: + { + size_t i; + struct possible_tag *match; + git_vector_foreach(&all_matches, i, match) { + git__free(match); + } + } + git_vector_free(&all_matches); + git_pqueue_free(&list); + git_revwalk_free(walk); + return error; +} + +static int normalize_options( + git_describe_options *dst, + const git_describe_options *src) +{ + git_describe_options default_options = GIT_DESCRIBE_OPTIONS_INIT; + if (!src) src = &default_options; + + *dst = *src; + + if (dst->max_candidates_tags > GIT_DESCRIBE_DEFAULT_MAX_CANDIDATES_TAGS) + dst->max_candidates_tags = GIT_DESCRIBE_DEFAULT_MAX_CANDIDATES_TAGS; + + return 0; +} + +int git_describe_commit( + git_describe_result **result, + git_object *committish, + git_describe_options *opts) +{ + struct get_name_data data; + struct commit_name *name; + git_commit *commit; + int error = -1; + git_describe_options normalized; + + GIT_ASSERT_ARG(result); + GIT_ASSERT_ARG(committish); + + data.result = git__calloc(1, sizeof(git_describe_result)); + GIT_ERROR_CHECK_ALLOC(data.result); + data.result->repo = git_object_owner(committish); + + data.repo = git_object_owner(committish); + + if ((error = normalize_options(&normalized, opts)) < 0) + return error; + + GIT_ERROR_CHECK_VERSION( + &normalized, + GIT_DESCRIBE_OPTIONS_VERSION, + "git_describe_options"); + data.opts = &normalized; + + if ((error = git_oidmap_new(&data.names)) < 0) + return error; + + /** TODO: contains to be implemented */ + + if ((error = git_object_peel((git_object **)(&commit), committish, GIT_OBJECT_COMMIT)) < 0) + goto cleanup; + + if ((error = git_reference_foreach_name( + git_object_owner(committish), + get_name, &data)) < 0) + goto cleanup; + + if (git_oidmap_size(data.names) == 0 && !normalized.show_commit_oid_as_fallback) { + git_error_set(GIT_ERROR_DESCRIBE, "cannot describe - " + "no reference found, cannot describe anything."); + error = -1; + goto cleanup; + } + + if ((error = describe(&data, commit)) < 0) + goto cleanup; + +cleanup: + git_commit_free(commit); + + git_oidmap_foreach_value(data.names, name, { + git_tag_free(name->tag); + git__free(name->path); + git__free(name); + }); + + git_oidmap_free(data.names); + + if (error < 0) + git_describe_result_free(data.result); + else + *result = data.result; + + return error; +} + +int git_describe_workdir( + git_describe_result **out, + git_repository *repo, + git_describe_options *opts) +{ + int error; + git_oid current_id; + git_status_list *status = NULL; + git_status_options status_opts = GIT_STATUS_OPTIONS_INIT; + git_describe_result *result = NULL; + git_object *commit; + + if ((error = git_reference_name_to_id(¤t_id, repo, GIT_HEAD_FILE)) < 0) + return error; + + if ((error = git_object_lookup(&commit, repo, ¤t_id, GIT_OBJECT_COMMIT)) < 0) + return error; + + /* The first step is to perform a describe of HEAD, so we can leverage this */ + if ((error = git_describe_commit(&result, commit, opts)) < 0) + goto out; + + if ((error = git_status_list_new(&status, repo, &status_opts)) < 0) + goto out; + + + if (git_status_list_entrycount(status) > 0) + result->dirty = 1; + +out: + git_object_free(commit); + git_status_list_free(status); + + if (error < 0) + git_describe_result_free(result); + else + *out = result; + + return error; +} + +static int normalize_format_options( + git_describe_format_options *dst, + const git_describe_format_options *src) +{ + if (!src) { + git_describe_format_options_init(dst, GIT_DESCRIBE_FORMAT_OPTIONS_VERSION); + return 0; + } + + memcpy(dst, src, sizeof(git_describe_format_options)); + return 0; +} + +static int git_describe__format( + git_str *out, + const git_describe_result *result, + const git_describe_format_options *given) +{ + int error; + git_repository *repo; + struct commit_name *name; + git_describe_format_options opts; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(result); + + GIT_ERROR_CHECK_VERSION(given, GIT_DESCRIBE_FORMAT_OPTIONS_VERSION, "git_describe_format_options"); + normalize_format_options(&opts, given); + + if (opts.always_use_long_format && opts.abbreviated_size == 0) { + git_error_set(GIT_ERROR_DESCRIBE, "cannot describe - " + "'always_use_long_format' is incompatible with a zero" + "'abbreviated_size'"); + return -1; + } + + + repo = result->repo; + + /* If we did find an exact match, then it's the easier method */ + if (result->exact_match) { + name = result->name; + if ((error = display_name(out, repo, name)) < 0) + return error; + + if (opts.always_use_long_format) { + const git_oid *id = name->tag ? git_tag_target_id(name->tag) : &result->commit_id; + if ((error = show_suffix(out, 0, repo, id, opts.abbreviated_size)) < 0) + return error; + } + + if (result->dirty && opts.dirty_suffix) + git_str_puts(out, opts.dirty_suffix); + + return git_str_oom(out) ? -1 : 0; + } + + /* If we didn't find *any* tags, we fall back to the commit's id */ + if (result->fallback_to_id) { + char hex_oid[GIT_OID_MAX_HEXSIZE + 1] = {0}; + int size = 0; + + if ((error = find_unique_abbrev_size( + &size, repo, &result->commit_id, opts.abbreviated_size)) < 0) + return -1; + + git_oid_fmt(hex_oid, &result->commit_id); + git_str_put(out, hex_oid, size); + + if (result->dirty && opts.dirty_suffix) + git_str_puts(out, opts.dirty_suffix); + + return git_str_oom(out) ? -1 : 0; + } + + /* Lastly, if we found a matching tag, we show that */ + name = result->tag->name; + + if ((error = display_name(out, repo, name)) < 0) + return error; + + if (opts.abbreviated_size) { + if ((error = show_suffix(out, result->tag->depth, repo, + &result->commit_id, opts.abbreviated_size)) < 0) + return error; + } + + if (result->dirty && opts.dirty_suffix) { + git_str_puts(out, opts.dirty_suffix); + } + + return git_str_oom(out) ? -1 : 0; +} + +int git_describe_format( + git_buf *out, + const git_describe_result *result, + const git_describe_format_options *given) +{ + GIT_BUF_WRAP_PRIVATE(out, git_describe__format, result, given); +} + +void git_describe_result_free(git_describe_result *result) +{ + if (result == NULL) + return; + + if (result->name) { + git_tag_free(result->name->tag); + git__free(result->name->path); + git__free(result->name); + } + + if (result->tag) { + git_tag_free(result->tag->name->tag); + git__free(result->tag->name->path); + git__free(result->tag->name); + git__free(result->tag); + } + + git__free(result); +} + +int git_describe_options_init(git_describe_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_describe_options, GIT_DESCRIBE_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_describe_init_options(git_describe_options *opts, unsigned int version) +{ + return git_describe_options_init(opts, version); +} +#endif + +int git_describe_format_options_init(git_describe_format_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_describe_format_options, GIT_DESCRIBE_FORMAT_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_describe_init_format_options(git_describe_format_options *opts, unsigned int version) +{ + return git_describe_format_options_init(opts, version); +} +#endif diff --git a/src/libgit2/diff.c b/src/libgit2/diff.c new file mode 100644 index 0000000..db12ccd --- /dev/null +++ b/src/libgit2/diff.c @@ -0,0 +1,402 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "diff.h" + +#include "common.h" +#include "buf.h" +#include "patch.h" +#include "email.h" +#include "commit.h" +#include "index.h" +#include "diff_generate.h" + +#include "git2/version.h" +#include "git2/email.h" + +struct patch_id_args { + git_diff *diff; + git_hash_ctx ctx; + git_oid result; + git_oid_t oid_type; + int first_file; +}; + +GIT_INLINE(const char *) diff_delta__path(const git_diff_delta *delta) +{ + const char *str = delta->old_file.path; + + if (!str || + delta->status == GIT_DELTA_ADDED || + delta->status == GIT_DELTA_RENAMED || + delta->status == GIT_DELTA_COPIED) + str = delta->new_file.path; + + return str; +} + +int git_diff_delta__cmp(const void *a, const void *b) +{ + const git_diff_delta *da = a, *db = b; + int val = strcmp(diff_delta__path(da), diff_delta__path(db)); + return val ? val : ((int)da->status - (int)db->status); +} + +int git_diff_delta__casecmp(const void *a, const void *b) +{ + const git_diff_delta *da = a, *db = b; + int val = strcasecmp(diff_delta__path(da), diff_delta__path(db)); + return val ? val : ((int)da->status - (int)db->status); +} + +int git_diff__entry_cmp(const void *a, const void *b) +{ + const git_index_entry *entry_a = a; + const git_index_entry *entry_b = b; + + return strcmp(entry_a->path, entry_b->path); +} + +int git_diff__entry_icmp(const void *a, const void *b) +{ + const git_index_entry *entry_a = a; + const git_index_entry *entry_b = b; + + return strcasecmp(entry_a->path, entry_b->path); +} + +void git_diff_free(git_diff *diff) +{ + if (!diff) + return; + + GIT_REFCOUNT_DEC(diff, diff->free_fn); +} + +void git_diff_addref(git_diff *diff) +{ + GIT_REFCOUNT_INC(diff); +} + +size_t git_diff_num_deltas(const git_diff *diff) +{ + GIT_ASSERT_ARG(diff); + return diff->deltas.length; +} + +size_t git_diff_num_deltas_of_type(const git_diff *diff, git_delta_t type) +{ + size_t i, count = 0; + const git_diff_delta *delta; + + GIT_ASSERT_ARG(diff); + + git_vector_foreach(&diff->deltas, i, delta) { + count += (delta->status == type); + } + + return count; +} + +const git_diff_delta *git_diff_get_delta(const git_diff *diff, size_t idx) +{ + GIT_ASSERT_ARG_WITH_RETVAL(diff, NULL); + return git_vector_get(&diff->deltas, idx); +} + +int git_diff_is_sorted_icase(const git_diff *diff) +{ + return (diff->opts.flags & GIT_DIFF_IGNORE_CASE) != 0; +} + +int git_diff_get_perfdata(git_diff_perfdata *out, const git_diff *diff) +{ + GIT_ASSERT_ARG(out); + GIT_ERROR_CHECK_VERSION(out, GIT_DIFF_PERFDATA_VERSION, "git_diff_perfdata"); + out->stat_calls = diff->perf.stat_calls; + out->oid_calculations = diff->perf.oid_calculations; + return 0; +} + +int git_diff_foreach( + git_diff *diff, + git_diff_file_cb file_cb, + git_diff_binary_cb binary_cb, + git_diff_hunk_cb hunk_cb, + git_diff_line_cb data_cb, + void *payload) +{ + int error = 0; + git_diff_delta *delta; + size_t idx; + + GIT_ASSERT_ARG(diff); + + git_vector_foreach(&diff->deltas, idx, delta) { + git_patch *patch; + + /* check flags against patch status */ + if (git_diff_delta__should_skip(&diff->opts, delta)) + continue; + + if ((error = git_patch_from_diff(&patch, diff, idx)) != 0) + break; + + error = git_patch__invoke_callbacks(patch, file_cb, binary_cb, + hunk_cb, data_cb, payload); + git_patch_free(patch); + + if (error) + break; + } + + return error; +} + +#ifndef GIT_DEPRECATE_HARD + +int git_diff_format_email( + git_buf *out, + git_diff *diff, + const git_diff_format_email_options *opts) +{ + git_email_create_options email_create_opts = GIT_EMAIL_CREATE_OPTIONS_INIT; + git_str email = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(diff); + GIT_ASSERT_ARG(opts && opts->summary && opts->id && opts->author); + + GIT_ERROR_CHECK_VERSION(opts, + GIT_DIFF_FORMAT_EMAIL_OPTIONS_VERSION, + "git_format_email_options"); + + /* This is a `git_buf` special case; subsequent calls append. */ + email.ptr = out->ptr; + email.asize = out->reserved; + email.size = out->size; + + out->ptr = git_str__initstr; + out->reserved = 0; + out->size = 0; + + if ((opts->flags & GIT_DIFF_FORMAT_EMAIL_EXCLUDE_SUBJECT_PATCH_MARKER) != 0) + email_create_opts.subject_prefix = ""; + + error = git_email__append_from_diff(&email, diff, opts->patch_no, + opts->total_patches, opts->id, opts->summary, opts->body, + opts->author, &email_create_opts); + + if (error < 0) + goto done; + + error = git_buf_fromstr(out, &email); + +done: + git_str_dispose(&email); + return error; +} + +int git_diff_commit_as_email( + git_buf *out, + git_repository *repo, + git_commit *commit, + size_t patch_no, + size_t total_patches, + uint32_t flags, + const git_diff_options *diff_opts) +{ + git_diff *diff = NULL; + git_email_create_options opts = GIT_EMAIL_CREATE_OPTIONS_INIT; + const git_oid *commit_id; + const char *summary, *body; + const git_signature *author; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(commit); + + commit_id = git_commit_id(commit); + summary = git_commit_summary(commit); + body = git_commit_body(commit); + author = git_commit_author(commit); + + if ((flags & GIT_DIFF_FORMAT_EMAIL_EXCLUDE_SUBJECT_PATCH_MARKER) != 0) + opts.subject_prefix = ""; + + if ((error = git_diff__commit(&diff, repo, commit, diff_opts)) < 0) + return error; + + error = git_email_create_from_diff(out, diff, patch_no, total_patches, commit_id, summary, body, author, &opts); + + git_diff_free(diff); + return error; +} + +int git_diff_init_options(git_diff_options *opts, unsigned int version) +{ + return git_diff_options_init(opts, version); +} + +int git_diff_find_init_options( + git_diff_find_options *opts, unsigned int version) +{ + return git_diff_find_options_init(opts, version); +} + +int git_diff_format_email_options_init( + git_diff_format_email_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_diff_format_email_options, + GIT_DIFF_FORMAT_EMAIL_OPTIONS_INIT); + return 0; +} + +int git_diff_format_email_init_options( + git_diff_format_email_options *opts, unsigned int version) +{ + return git_diff_format_email_options_init(opts, version); +} + +#endif + +int git_diff_options_init(git_diff_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_diff_options, GIT_DIFF_OPTIONS_INIT); + return 0; +} + +int git_diff_find_options_init( + git_diff_find_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_diff_find_options, GIT_DIFF_FIND_OPTIONS_INIT); + return 0; +} + +static int flush_hunk(git_oid *result, struct patch_id_args *args) +{ + git_hash_ctx *ctx = &args->ctx; + git_oid hash; + unsigned short carry = 0; + size_t i; + int error; + + if ((error = git_hash_final(hash.id, ctx)) < 0 || + (error = git_hash_init(ctx)) < 0) + return error; + + for (i = 0; i < git_oid_size(args->oid_type); i++) { + carry += result->id[i] + hash.id[i]; + result->id[i] = (unsigned char)carry; + carry >>= 8; + } + + return 0; +} + +static void strip_spaces(git_str *buf) +{ + char *src = buf->ptr, *dst = buf->ptr; + char c; + size_t len = 0; + + while ((c = *src++) != '\0') { + if (!git__isspace(c)) { + *dst++ = c; + len++; + } + } + + git_str_truncate(buf, len); +} + +static int diff_patchid_print_callback_to_buf( + const git_diff_delta *delta, + const git_diff_hunk *hunk, + const git_diff_line *line, + void *payload) +{ + struct patch_id_args *args = (struct patch_id_args *) payload; + git_str buf = GIT_STR_INIT; + int error = 0; + + if (line->origin == GIT_DIFF_LINE_CONTEXT_EOFNL || + line->origin == GIT_DIFF_LINE_ADD_EOFNL || + line->origin == GIT_DIFF_LINE_DEL_EOFNL) + goto out; + + if ((error = git_diff_print_callback__to_buf(delta, hunk, + line, &buf)) < 0) + goto out; + + strip_spaces(&buf); + + if (line->origin == GIT_DIFF_LINE_FILE_HDR && + !args->first_file && + (error = flush_hunk(&args->result, args) < 0)) + goto out; + + if ((error = git_hash_update(&args->ctx, buf.ptr, buf.size)) < 0) + goto out; + + if (line->origin == GIT_DIFF_LINE_FILE_HDR && args->first_file) + args->first_file = 0; + +out: + git_str_dispose(&buf); + return error; +} + +int git_diff_patchid_options_init(git_diff_patchid_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_diff_patchid_options, GIT_DIFF_PATCHID_OPTIONS_INIT); + return 0; +} + +int git_diff_patchid(git_oid *out, git_diff *diff, git_diff_patchid_options *opts) +{ + struct patch_id_args args; + git_hash_algorithm_t algorithm; + int error; + + GIT_ERROR_CHECK_VERSION( + opts, GIT_DIFF_PATCHID_OPTIONS_VERSION, "git_diff_patchid_options"); + + algorithm = git_oid_algorithm(diff->opts.oid_type); + + memset(&args, 0, sizeof(args)); + args.diff = diff; + args.first_file = 1; + args.oid_type = diff->opts.oid_type; + if ((error = git_hash_ctx_init(&args.ctx, algorithm)) < 0) + goto out; + + if ((error = git_diff_print(diff, + GIT_DIFF_FORMAT_PATCH_ID, + diff_patchid_print_callback_to_buf, + &args)) < 0) + goto out; + + if ((error = (flush_hunk(&args.result, &args))) < 0) + goto out; + +#ifdef GIT_EXPERIMENTAL_SHA256 + args.result.type = diff->opts.oid_type; +#endif + + git_oid_cpy(out, &args.result); + +out: + git_hash_ctx_cleanup(&args.ctx); + return error; +} diff --git a/src/libgit2/diff.h b/src/libgit2/diff.h new file mode 100644 index 0000000..f21b276 --- /dev/null +++ b/src/libgit2/diff.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_diff_h__ +#define INCLUDE_diff_h__ + +#include "common.h" + +#include "git2/diff.h" +#include "git2/patch.h" +#include "git2/sys/diff.h" +#include "git2/oid.h" + +#include "vector.h" +#include "iterator.h" +#include "repository.h" +#include "pool.h" +#include "odb.h" + +#define DIFF_OLD_PREFIX_DEFAULT "a/" +#define DIFF_NEW_PREFIX_DEFAULT "b/" + +typedef enum { + GIT_DIFF_TYPE_UNKNOWN = 0, + GIT_DIFF_TYPE_GENERATED = 1, + GIT_DIFF_TYPE_PARSED = 2 +} git_diff_origin_t; + +struct git_diff { + git_refcount rc; + git_repository *repo; + git_attr_session attrsession; + git_diff_origin_t type; + git_diff_options opts; + git_vector deltas; /* vector of git_diff_delta */ + git_pool pool; + git_iterator_t old_src; + git_iterator_t new_src; + git_diff_perfdata perf; + + int (*strcomp)(const char *, const char *); + int (*strncomp)(const char *, const char *, size_t); + int (*pfxcomp)(const char *str, const char *pfx); + int (*entrycomp)(const void *a, const void *b); + + int (*patch_fn)(git_patch **out, git_diff *diff, size_t idx); + void (*free_fn)(git_diff *diff); +}; + +extern int git_diff_delta__format_file_header( + git_str *out, + const git_diff_delta *delta, + const char *oldpfx, + const char *newpfx, + int oid_strlen, + bool print_index); + +extern int git_diff_delta__cmp(const void *a, const void *b); +extern int git_diff_delta__casecmp(const void *a, const void *b); + +extern int git_diff__entry_cmp(const void *a, const void *b); +extern int git_diff__entry_icmp(const void *a, const void *b); + +#endif diff --git a/src/libgit2/diff_driver.c b/src/libgit2/diff_driver.c new file mode 100644 index 0000000..5f25fdb --- /dev/null +++ b/src/libgit2/diff_driver.c @@ -0,0 +1,522 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "diff_driver.h" + +#include "git2/attr.h" + +#include "common.h" +#include "diff.h" +#include "strmap.h" +#include "map.h" +#include "config.h" +#include "regexp.h" +#include "repository.h" + +typedef enum { + DIFF_DRIVER_AUTO = 0, + DIFF_DRIVER_BINARY = 1, + DIFF_DRIVER_TEXT = 2, + DIFF_DRIVER_PATTERNLIST = 3 +} git_diff_driver_t; + +typedef struct { + git_regexp re; + int flags; +} git_diff_driver_pattern; + +enum { + REG_NEGATE = (1 << 15) /* get out of the way of existing flags */ +}; + +/* data for finding function context for a given file type */ +struct git_diff_driver { + git_diff_driver_t type; + uint32_t binary_flags; + uint32_t other_flags; + git_array_t(git_diff_driver_pattern) fn_patterns; + git_regexp word_pattern; + char name[GIT_FLEX_ARRAY]; +}; + +#include "userdiff.h" + +struct git_diff_driver_registry { + git_strmap *drivers; +}; + +#define FORCE_DIFFABLE (GIT_DIFF_FORCE_TEXT | GIT_DIFF_FORCE_BINARY) + +static git_diff_driver diff_driver_auto = { DIFF_DRIVER_AUTO, 0, 0 }; +static git_diff_driver diff_driver_binary = { DIFF_DRIVER_BINARY, GIT_DIFF_FORCE_BINARY, 0 }; +static git_diff_driver diff_driver_text = { DIFF_DRIVER_TEXT, GIT_DIFF_FORCE_TEXT, 0 }; + +git_diff_driver_registry *git_diff_driver_registry_new(void) +{ + git_diff_driver_registry *reg = + git__calloc(1, sizeof(git_diff_driver_registry)); + if (!reg) + return NULL; + + if (git_strmap_new(®->drivers) < 0) { + git_diff_driver_registry_free(reg); + return NULL; + } + + return reg; +} + +void git_diff_driver_registry_free(git_diff_driver_registry *reg) +{ + git_diff_driver *drv; + + if (!reg) + return; + + git_strmap_foreach_value(reg->drivers, drv, git_diff_driver_free(drv)); + git_strmap_free(reg->drivers); + git__free(reg); +} + +static int diff_driver_add_patterns( + git_diff_driver *drv, const char *regex_str, int regex_flags) +{ + int error = 0; + const char *scan, *end; + git_diff_driver_pattern *pat = NULL; + git_str buf = GIT_STR_INIT; + + for (scan = regex_str; scan; scan = end) { + /* get pattern to fill in */ + if ((pat = git_array_alloc(drv->fn_patterns)) == NULL) { + return -1; + } + + pat->flags = regex_flags; + if (*scan == '!') { + pat->flags |= REG_NEGATE; + ++scan; + } + + if ((end = strchr(scan, '\n')) != NULL) { + error = git_str_set(&buf, scan, end - scan); + end++; + } else { + error = git_str_sets(&buf, scan); + } + if (error < 0) + break; + + if ((error = git_regexp_compile(&pat->re, buf.ptr, regex_flags)) != 0) { + /* + * TODO: issue a warning + */ + } + } + + if (error && pat != NULL) + (void)git_array_pop(drv->fn_patterns); /* release last item */ + git_str_dispose(&buf); + + /* We want to ignore bad patterns, so return success regardless */ + return 0; +} + +static int diff_driver_xfuncname(const git_config_entry *entry, void *payload) +{ + return diff_driver_add_patterns(payload, entry->value, 0); +} + +static int diff_driver_funcname(const git_config_entry *entry, void *payload) +{ + return diff_driver_add_patterns(payload, entry->value, 0); +} + +static git_diff_driver_registry *git_repository_driver_registry( + git_repository *repo) +{ + git_diff_driver_registry *reg = git_atomic_load(repo->diff_drivers), *newreg; + if (reg) + return reg; + + newreg = git_diff_driver_registry_new(); + if (!newreg) { + git_error_set(GIT_ERROR_REPOSITORY, "unable to create diff driver registry"); + return newreg; + } + reg = git_atomic_compare_and_swap(&repo->diff_drivers, NULL, newreg); + if (!reg) { + reg = newreg; + } else { + /* if we race, free losing allocation */ + git_diff_driver_registry_free(newreg); + } + return reg; +} + +static int diff_driver_alloc( + git_diff_driver **out, size_t *namelen_out, const char *name) +{ + git_diff_driver *driver; + size_t driverlen = sizeof(git_diff_driver), + namelen = strlen(name), + alloclen; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, driverlen, namelen); + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, 1); + + driver = git__calloc(1, alloclen); + GIT_ERROR_CHECK_ALLOC(driver); + + memcpy(driver->name, name, namelen); + + *out = driver; + + if (namelen_out) + *namelen_out = namelen; + + return 0; +} + +static int git_diff_driver_builtin( + git_diff_driver **out, + git_diff_driver_registry *reg, + const char *driver_name) +{ + git_diff_driver_definition *ddef = NULL; + git_diff_driver *drv = NULL; + int error = 0; + size_t idx; + + for (idx = 0; idx < ARRAY_SIZE(builtin_defs); ++idx) { + if (!strcasecmp(driver_name, builtin_defs[idx].name)) { + ddef = &builtin_defs[idx]; + break; + } + } + if (!ddef) + goto done; + + if ((error = diff_driver_alloc(&drv, NULL, ddef->name)) < 0) + goto done; + + drv->type = DIFF_DRIVER_PATTERNLIST; + + if (ddef->fns && + (error = diff_driver_add_patterns( + drv, ddef->fns, ddef->flags)) < 0) + goto done; + + if (ddef->words && + (error = git_regexp_compile(&drv->word_pattern, ddef->words, ddef->flags)) < 0) + goto done; + + if ((error = git_strmap_set(reg->drivers, drv->name, drv)) < 0) + goto done; + +done: + if (error && drv) + git_diff_driver_free(drv); + else + *out = drv; + + return error; +} + +static int git_diff_driver_load( + git_diff_driver **out, git_repository *repo, const char *driver_name) +{ + int error = 0; + git_diff_driver_registry *reg; + git_diff_driver *drv; + size_t namelen; + git_config *cfg = NULL; + git_str name = GIT_STR_INIT; + git_config_entry *ce = NULL; + bool found_driver = false; + + if ((reg = git_repository_driver_registry(repo)) == NULL) + return -1; + + if ((drv = git_strmap_get(reg->drivers, driver_name)) != NULL) { + *out = drv; + return 0; + } + + if ((error = diff_driver_alloc(&drv, &namelen, driver_name)) < 0) + goto done; + + drv->type = DIFF_DRIVER_AUTO; + + /* if you can't read config for repo, just use default driver */ + if (git_repository_config_snapshot(&cfg, repo) < 0) { + git_error_clear(); + goto done; + } + + if ((error = git_str_printf(&name, "diff.%s.binary", driver_name)) < 0) + goto done; + + switch (git_config__get_bool_force(cfg, name.ptr, -1)) { + case true: + /* if diff..binary is true, just return the binary driver */ + *out = &diff_driver_binary; + goto done; + case false: + /* if diff..binary is false, force binary checks off */ + /* but still may have custom function context patterns, etc. */ + drv->binary_flags = GIT_DIFF_FORCE_TEXT; + found_driver = true; + break; + default: + /* diff..binary unspecified or "auto", so just continue */ + break; + } + + /* TODO: warn if diff..command or diff..textconv are set */ + + git_str_truncate(&name, namelen + strlen("diff..")); + if ((error = git_str_PUTS(&name, "xfuncname")) < 0) + goto done; + + if ((error = git_config_get_multivar_foreach( + cfg, name.ptr, NULL, diff_driver_xfuncname, drv)) < 0) { + if (error != GIT_ENOTFOUND) + goto done; + git_error_clear(); /* no diff..xfuncname, so just continue */ + } + + git_str_truncate(&name, namelen + strlen("diff..")); + if ((error = git_str_PUTS(&name, "funcname")) < 0) + goto done; + + if ((error = git_config_get_multivar_foreach( + cfg, name.ptr, NULL, diff_driver_funcname, drv)) < 0) { + if (error != GIT_ENOTFOUND) + goto done; + git_error_clear(); /* no diff..funcname, so just continue */ + } + + /* if we found any patterns, set driver type to use correct callback */ + if (git_array_size(drv->fn_patterns) > 0) { + drv->type = DIFF_DRIVER_PATTERNLIST; + found_driver = true; + } + + git_str_truncate(&name, namelen + strlen("diff..")); + if ((error = git_str_PUTS(&name, "wordregex")) < 0) + goto done; + + if ((error = git_config__lookup_entry(&ce, cfg, name.ptr, false)) < 0) + goto done; + if (!ce || !ce->value) + /* no diff..wordregex, so just continue */; + else if (!(error = git_regexp_compile(&drv->word_pattern, ce->value, 0))) + found_driver = true; + else { + /* TODO: warn about bad regex instead of failure */ + goto done; + } + + /* TODO: look up diff..algorithm to turn on minimal / patience + * diff in drv->other_flags + */ + + /* if no driver config found at all, fall back on AUTO driver */ + if (!found_driver) + goto done; + + /* store driver in registry */ + if ((error = git_strmap_set(reg->drivers, drv->name, drv)) < 0) + goto done; + + *out = drv; + +done: + git_config_entry_free(ce); + git_str_dispose(&name); + git_config_free(cfg); + + if (!*out) { + int error2 = git_diff_driver_builtin(out, reg, driver_name); + if (!error) + error = error2; + } + + if (drv && drv != *out) + git_diff_driver_free(drv); + + return error; +} + +int git_diff_driver_lookup( + git_diff_driver **out, git_repository *repo, + git_attr_session *attrsession, const char *path) +{ + int error = 0; + const char *values[1], *attrs[] = { "diff" }; + + GIT_ASSERT_ARG(out); + *out = NULL; + + if (!repo || !path || !strlen(path)) + /* just use the auto value */; + else if ((error = git_attr_get_many_with_session(values, repo, + attrsession, 0, path, 1, attrs)) < 0) + /* return error below */; + + else if (GIT_ATTR_IS_UNSPECIFIED(values[0])) + /* just use the auto value */; + else if (GIT_ATTR_IS_FALSE(values[0])) + *out = &diff_driver_binary; + else if (GIT_ATTR_IS_TRUE(values[0])) + *out = &diff_driver_text; + + /* otherwise look for driver information in config and build driver */ + else if ((error = git_diff_driver_load(out, repo, values[0])) < 0) { + if (error == GIT_ENOTFOUND) { + error = 0; + git_error_clear(); + } + } + + if (!*out) + *out = &diff_driver_auto; + + return error; +} + +void git_diff_driver_free(git_diff_driver *driver) +{ + git_diff_driver_pattern *pat; + + if (!driver) + return; + + while ((pat = git_array_pop(driver->fn_patterns)) != NULL) + git_regexp_dispose(&pat->re); + git_array_clear(driver->fn_patterns); + + git_regexp_dispose(&driver->word_pattern); + + git__free(driver); +} + +void git_diff_driver_update_options( + uint32_t *option_flags, git_diff_driver *driver) +{ + if ((*option_flags & FORCE_DIFFABLE) == 0) + *option_flags |= driver->binary_flags; + + *option_flags |= driver->other_flags; +} + +int git_diff_driver_content_is_binary( + git_diff_driver *driver, const char *content, size_t content_len) +{ + git_str search = GIT_STR_INIT; + + GIT_UNUSED(driver); + + git_str_attach_notowned(&search, content, + min(content_len, GIT_FILTER_BYTES_TO_CHECK_NUL)); + + /* TODO: provide encoding / binary detection callbacks that can + * be UTF-8 aware, etc. For now, instead of trying to be smart, + * let's just use the simple NUL-byte detection that core git uses. + */ + + /* previously was: if (git_str_is_binary(&search)) */ + if (git_str_contains_nul(&search)) + return 1; + + return 0; +} + +static int diff_context_line__simple( + git_diff_driver *driver, git_str *line) +{ + char firstch = line->ptr[0]; + GIT_UNUSED(driver); + return (git__isalpha(firstch) || firstch == '_' || firstch == '$'); +} + +static int diff_context_line__pattern_match( + git_diff_driver *driver, git_str *line) +{ + size_t i, maxi = git_array_size(driver->fn_patterns); + git_regmatch pmatch[2]; + + for (i = 0; i < maxi; ++i) { + git_diff_driver_pattern *pat = git_array_get(driver->fn_patterns, i); + + if (!git_regexp_search(&pat->re, line->ptr, 2, pmatch)) { + if (pat->flags & REG_NEGATE) + return false; + + /* use pmatch data to trim line data */ + i = (pmatch[1].start >= 0) ? 1 : 0; + git_str_consume(line, git_str_cstr(line) + pmatch[i].start); + git_str_truncate(line, pmatch[i].end - pmatch[i].start); + git_str_rtrim(line); + + return true; + } + } + + return false; +} + +static long diff_context_find( + const char *line, + long line_len, + char *out, + long out_size, + void *payload) +{ + git_diff_find_context_payload *ctxt = payload; + + if (git_str_set(&ctxt->line, line, (size_t)line_len) < 0) + return -1; + git_str_rtrim(&ctxt->line); + + if (!ctxt->line.size) + return -1; + + if (!ctxt->match_line || !ctxt->match_line(ctxt->driver, &ctxt->line)) + return -1; + + if (out_size > (long)ctxt->line.size) + out_size = (long)ctxt->line.size; + memcpy(out, ctxt->line.ptr, (size_t)out_size); + + return out_size; +} + +void git_diff_find_context_init( + git_diff_find_context_fn *findfn_out, + git_diff_find_context_payload *payload_out, + git_diff_driver *driver) +{ + *findfn_out = driver ? diff_context_find : NULL; + + memset(payload_out, 0, sizeof(*payload_out)); + if (driver) { + payload_out->driver = driver; + payload_out->match_line = (driver->type == DIFF_DRIVER_PATTERNLIST) ? + diff_context_line__pattern_match : diff_context_line__simple; + git_str_init(&payload_out->line, 0); + } +} + +void git_diff_find_context_clear(git_diff_find_context_payload *payload) +{ + if (payload) { + git_str_dispose(&payload->line); + payload->driver = NULL; + } +} diff --git a/src/libgit2/diff_driver.h b/src/libgit2/diff_driver.h new file mode 100644 index 0000000..03711e8 --- /dev/null +++ b/src/libgit2/diff_driver.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_diff_driver_h__ +#define INCLUDE_diff_driver_h__ + +#include "common.h" + +#include "attr_file.h" +#include "str.h" + +typedef struct git_diff_driver_registry git_diff_driver_registry; + +git_diff_driver_registry *git_diff_driver_registry_new(void); +void git_diff_driver_registry_free(git_diff_driver_registry *); + +typedef struct git_diff_driver git_diff_driver; + +int git_diff_driver_lookup(git_diff_driver **, git_repository *, + git_attr_session *attrsession, const char *); +void git_diff_driver_free(git_diff_driver *); + +/* diff option flags to force off and on for this driver */ +void git_diff_driver_update_options(uint32_t *option_flags, git_diff_driver *); + +/* returns -1 meaning "unknown", 0 meaning not binary, 1 meaning binary */ +int git_diff_driver_content_is_binary( + git_diff_driver *, const char *content, size_t content_len); + +typedef long (*git_diff_find_context_fn)( + const char *, long, char *, long, void *); + +typedef int (*git_diff_find_context_line)( + git_diff_driver *, git_str *); + +typedef struct { + git_diff_driver *driver; + git_diff_find_context_line match_line; + git_str line; +} git_diff_find_context_payload; + +void git_diff_find_context_init( + git_diff_find_context_fn *findfn_out, + git_diff_find_context_payload *payload_out, + git_diff_driver *driver); + +void git_diff_find_context_clear(git_diff_find_context_payload *); + +#endif diff --git a/src/libgit2/diff_file.c b/src/libgit2/diff_file.c new file mode 100644 index 0000000..a792834 --- /dev/null +++ b/src/libgit2/diff_file.c @@ -0,0 +1,490 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "diff_file.h" + +#include "git2/blob.h" +#include "git2/submodule.h" +#include "diff.h" +#include "diff_generate.h" +#include "odb.h" +#include "futils.h" +#include "filter.h" + +#define DIFF_MAX_FILESIZE 0x20000000 + +static bool diff_file_content_binary_by_size(git_diff_file_content *fc) +{ + /* if we have diff opts, check max_size vs file size */ + if ((fc->file->flags & DIFF_FLAGS_KNOWN_BINARY) == 0 && + fc->opts_max_size > 0 && + fc->file->size > fc->opts_max_size) + fc->file->flags |= GIT_DIFF_FLAG_BINARY; + + return ((fc->file->flags & GIT_DIFF_FLAG_BINARY) != 0); +} + +static void diff_file_content_binary_by_content(git_diff_file_content *fc) +{ + if ((fc->file->flags & DIFF_FLAGS_KNOWN_BINARY) != 0) + return; + + switch (git_diff_driver_content_is_binary( + fc->driver, fc->map.data, fc->map.len)) { + case 0: fc->file->flags |= GIT_DIFF_FLAG_NOT_BINARY; break; + case 1: fc->file->flags |= GIT_DIFF_FLAG_BINARY; break; + default: break; + } +} + +static int diff_file_content_init_common( + git_diff_file_content *fc, const git_diff_options *opts) +{ + fc->opts_flags = opts ? opts->flags : GIT_DIFF_NORMAL; + + if (opts && opts->max_size >= 0) + fc->opts_max_size = opts->max_size ? + opts->max_size : DIFF_MAX_FILESIZE; + + if (fc->src == GIT_ITERATOR_EMPTY) + fc->src = GIT_ITERATOR_TREE; + + if (!fc->driver && + git_diff_driver_lookup(&fc->driver, fc->repo, + NULL, fc->file->path) < 0) + return -1; + + /* give driver a chance to modify options */ + git_diff_driver_update_options(&fc->opts_flags, fc->driver); + + /* make sure file is conceivable mmap-able */ + if ((size_t)fc->file->size != fc->file->size) + fc->file->flags |= GIT_DIFF_FLAG_BINARY; + /* check if user is forcing text diff the file */ + else if (fc->opts_flags & GIT_DIFF_FORCE_TEXT) { + fc->file->flags &= ~GIT_DIFF_FLAG_BINARY; + fc->file->flags |= GIT_DIFF_FLAG_NOT_BINARY; + } + /* check if user is forcing binary diff the file */ + else if (fc->opts_flags & GIT_DIFF_FORCE_BINARY) { + fc->file->flags &= ~GIT_DIFF_FLAG_NOT_BINARY; + fc->file->flags |= GIT_DIFF_FLAG_BINARY; + } + + diff_file_content_binary_by_size(fc); + + if ((fc->flags & GIT_DIFF_FLAG__NO_DATA) != 0) { + fc->flags |= GIT_DIFF_FLAG__LOADED; + fc->map.len = 0; + fc->map.data = ""; + } + + if ((fc->flags & GIT_DIFF_FLAG__LOADED) != 0) + diff_file_content_binary_by_content(fc); + + return 0; +} + +int git_diff_file_content__init_from_diff( + git_diff_file_content *fc, + git_diff *diff, + git_diff_delta *delta, + bool use_old) +{ + bool has_data = true; + + memset(fc, 0, sizeof(*fc)); + fc->repo = diff->repo; + fc->file = use_old ? &delta->old_file : &delta->new_file; + fc->src = use_old ? diff->old_src : diff->new_src; + + if (git_diff_driver_lookup(&fc->driver, fc->repo, + &diff->attrsession, fc->file->path) < 0) + return -1; + + switch (delta->status) { + case GIT_DELTA_ADDED: + has_data = !use_old; break; + case GIT_DELTA_DELETED: + has_data = use_old; break; + case GIT_DELTA_UNTRACKED: + has_data = (use_old == (diff->opts.flags & GIT_DIFF_REVERSE)) && + (diff->opts.flags & GIT_DIFF_SHOW_UNTRACKED_CONTENT) != 0; + break; + case GIT_DELTA_UNREADABLE: + case GIT_DELTA_MODIFIED: + case GIT_DELTA_COPIED: + case GIT_DELTA_RENAMED: + break; + default: + has_data = false; + break; + } + + if (!has_data) + fc->flags |= GIT_DIFF_FLAG__NO_DATA; + + return diff_file_content_init_common(fc, &diff->opts); +} + +int git_diff_file_content__init_from_src( + git_diff_file_content *fc, + git_repository *repo, + const git_diff_options *opts, + const git_diff_file_content_src *src, + git_diff_file *as_file) +{ + memset(fc, 0, sizeof(*fc)); + fc->repo = repo; + fc->file = as_file; + + if (!src->blob && !src->buf) { + fc->flags |= GIT_DIFF_FLAG__NO_DATA; + git_oid_clear(&fc->file->id, opts->oid_type); + } else { + fc->flags |= GIT_DIFF_FLAG__LOADED; + fc->file->flags |= GIT_DIFF_FLAG_VALID_ID; + fc->file->mode = GIT_FILEMODE_BLOB; + + if (src->blob) { + git_blob_dup((git_blob **)&fc->blob, (git_blob *) src->blob); + fc->file->size = git_blob_rawsize(src->blob); + git_oid_cpy(&fc->file->id, git_blob_id(src->blob)); + fc->file->id_abbrev = (uint16_t)git_oid_hexsize(repo->oid_type); + + fc->map.len = (size_t)fc->file->size; + fc->map.data = (char *)git_blob_rawcontent(src->blob); + + fc->flags |= GIT_DIFF_FLAG__FREE_BLOB; + } else { + int error; + if ((error = git_odb__hash(&fc->file->id, src->buf, src->buflen, GIT_OBJECT_BLOB, opts->oid_type)) < 0) + return error; + fc->file->size = src->buflen; + fc->file->id_abbrev = (uint16_t)git_oid_hexsize(opts->oid_type); + + fc->map.len = src->buflen; + fc->map.data = (char *)src->buf; + } + } + + return diff_file_content_init_common(fc, opts); +} + +static int diff_file_content_commit_to_str( + git_diff_file_content *fc, bool check_status) +{ + char oid[GIT_OID_MAX_HEXSIZE+1]; + git_str content = GIT_STR_INIT; + const char *status = ""; + + if (check_status) { + int error = 0; + git_submodule *sm = NULL; + unsigned int sm_status = 0; + const git_oid *sm_head; + + if ((error = git_submodule_lookup(&sm, fc->repo, fc->file->path)) < 0) { + /* GIT_EEXISTS means a "submodule" that has not been git added */ + if (error == GIT_EEXISTS) { + git_error_clear(); + error = 0; + } + return error; + } + + if ((error = git_submodule_status(&sm_status, fc->repo, fc->file->path, GIT_SUBMODULE_IGNORE_UNSPECIFIED)) < 0) { + git_submodule_free(sm); + return error; + } + + /* update OID if we didn't have it previously */ + if ((fc->file->flags & GIT_DIFF_FLAG_VALID_ID) == 0 && + ((sm_head = git_submodule_wd_id(sm)) != NULL || + (sm_head = git_submodule_head_id(sm)) != NULL)) + { + git_oid_cpy(&fc->file->id, sm_head); + fc->file->flags |= GIT_DIFF_FLAG_VALID_ID; + } + + if (GIT_SUBMODULE_STATUS_IS_WD_DIRTY(sm_status)) + status = "-dirty"; + + git_submodule_free(sm); + } + + git_oid_tostr(oid, sizeof(oid), &fc->file->id); + if (git_str_printf(&content, "Subproject commit %s%s\n", oid, status) < 0) + return -1; + + fc->map.len = git_str_len(&content); + fc->map.data = git_str_detach(&content); + fc->flags |= GIT_DIFF_FLAG__FREE_DATA; + + return 0; +} + +static int diff_file_content_load_blob( + git_diff_file_content *fc, + git_diff_options *opts) +{ + int error = 0; + git_odb_object *odb_obj = NULL; + + if (git_oid_is_zero(&fc->file->id)) + return 0; + + if (fc->file->mode == GIT_FILEMODE_COMMIT) + return diff_file_content_commit_to_str(fc, false); + + /* if we don't know size, try to peek at object header first */ + if (!fc->file->size) { + if ((error = git_diff_file__resolve_zero_size( + fc->file, &odb_obj, fc->repo)) < 0) + return error; + } + + if ((opts->flags & GIT_DIFF_SHOW_BINARY) == 0 && + diff_file_content_binary_by_size(fc)) + return 0; + + if (odb_obj != NULL) { + error = git_object__from_odb_object( + (git_object **)&fc->blob, fc->repo, odb_obj, GIT_OBJECT_BLOB); + git_odb_object_free(odb_obj); + } else { + error = git_blob_lookup( + (git_blob **)&fc->blob, fc->repo, &fc->file->id); + } + + if (!error) { + fc->flags |= GIT_DIFF_FLAG__FREE_BLOB; + fc->map.data = (void *)git_blob_rawcontent(fc->blob); + fc->map.len = (size_t)git_blob_rawsize(fc->blob); + } + + return error; +} + +static int diff_file_content_load_workdir_symlink_fake( + git_diff_file_content *fc, git_str *path) +{ + git_str target = GIT_STR_INIT; + int error; + + if ((error = git_futils_readbuffer(&target, path->ptr)) < 0) + return error; + + fc->map.len = git_str_len(&target); + fc->map.data = git_str_detach(&target); + fc->flags |= GIT_DIFF_FLAG__FREE_DATA; + + git_str_dispose(&target); + return error; +} + +static int diff_file_content_load_workdir_symlink( + git_diff_file_content *fc, git_str *path) +{ + ssize_t alloc_len, read_len; + int symlink_supported, error; + + if ((error = git_repository__configmap_lookup( + &symlink_supported, fc->repo, GIT_CONFIGMAP_SYMLINKS)) < 0) + return -1; + + if (!symlink_supported) + return diff_file_content_load_workdir_symlink_fake(fc, path); + + /* link path on disk could be UTF-16, so prepare a buffer that is + * big enough to handle some UTF-8 data expansion + */ + alloc_len = (ssize_t)(fc->file->size * 2) + 1; + + fc->map.data = git__calloc(alloc_len, sizeof(char)); + GIT_ERROR_CHECK_ALLOC(fc->map.data); + + fc->flags |= GIT_DIFF_FLAG__FREE_DATA; + + read_len = p_readlink(git_str_cstr(path), fc->map.data, alloc_len); + if (read_len < 0) { + git_error_set(GIT_ERROR_OS, "failed to read symlink '%s'", fc->file->path); + return -1; + } + + fc->map.len = read_len; + return 0; +} + +static int diff_file_content_load_workdir_file( + git_diff_file_content *fc, + git_str *path, + git_diff_options *diff_opts) +{ + int error = 0; + git_filter_list *fl = NULL; + git_file fd = git_futils_open_ro(git_str_cstr(path)); + git_str raw = GIT_STR_INIT; + git_object_size_t new_file_size = 0; + + if (fd < 0) + return fd; + + error = git_futils_filesize(&new_file_size, fd); + + if (error < 0) + goto cleanup; + + if (!(fc->file->flags & GIT_DIFF_FLAG_VALID_SIZE)) { + fc->file->size = new_file_size; + fc->file->flags |= GIT_DIFF_FLAG_VALID_SIZE; + } else if (fc->file->size != new_file_size) { + git_error_set(GIT_ERROR_FILESYSTEM, "file changed before we could read it"); + error = -1; + goto cleanup; + } + + /* if file is empty, don't attempt to mmap or readbuffer */ + if (fc->file->size == 0) { + fc->map.len = 0; + fc->map.data = git_str__initstr; + goto cleanup; + } + + if ((diff_opts->flags & GIT_DIFF_SHOW_BINARY) == 0 && + diff_file_content_binary_by_size(fc)) + goto cleanup; + + if ((error = git_filter_list_load( + &fl, fc->repo, NULL, fc->file->path, + GIT_FILTER_TO_ODB, GIT_FILTER_ALLOW_UNSAFE)) < 0) + goto cleanup; + + /* if there are no filters, try to mmap the file */ + if (fl == NULL) { + if (!(error = git_futils_mmap_ro( + &fc->map, fd, 0, (size_t)fc->file->size))) { + fc->flags |= GIT_DIFF_FLAG__UNMAP_DATA; + goto cleanup; + } + + /* if mmap failed, fall through to try readbuffer below */ + git_error_clear(); + } + + if (!(error = git_futils_readbuffer_fd(&raw, fd, (size_t)fc->file->size))) { + git_str out = GIT_STR_INIT; + + error = git_filter_list__convert_buf(&out, fl, &raw); + + if (!error) { + fc->map.len = out.size; + fc->map.data = out.ptr; + fc->flags |= GIT_DIFF_FLAG__FREE_DATA; + } + } + +cleanup: + git_filter_list_free(fl); + p_close(fd); + + return error; +} + +static int diff_file_content_load_workdir( + git_diff_file_content *fc, + git_diff_options *diff_opts) +{ + int error = 0; + git_str path = GIT_STR_INIT; + + if (fc->file->mode == GIT_FILEMODE_COMMIT) + return diff_file_content_commit_to_str(fc, true); + + if (fc->file->mode == GIT_FILEMODE_TREE) + return 0; + + if (git_repository_workdir_path(&path, fc->repo, fc->file->path) < 0) + return -1; + + if (S_ISLNK(fc->file->mode)) + error = diff_file_content_load_workdir_symlink(fc, &path); + else + error = diff_file_content_load_workdir_file(fc, &path, diff_opts); + + /* once data is loaded, update OID if we didn't have it previously */ + if (!error && (fc->file->flags & GIT_DIFF_FLAG_VALID_ID) == 0) { + error = git_odb__hash( + &fc->file->id, fc->map.data, fc->map.len, + GIT_OBJECT_BLOB, diff_opts->oid_type); + fc->file->flags |= GIT_DIFF_FLAG_VALID_ID; + } + + git_str_dispose(&path); + return error; +} + +int git_diff_file_content__load( + git_diff_file_content *fc, + git_diff_options *diff_opts) +{ + int error = 0; + + if ((fc->flags & GIT_DIFF_FLAG__LOADED) != 0) + return 0; + + if ((fc->file->flags & GIT_DIFF_FLAG_BINARY) != 0 && + (diff_opts->flags & GIT_DIFF_SHOW_BINARY) == 0) + return 0; + + if (fc->src == GIT_ITERATOR_WORKDIR) + error = diff_file_content_load_workdir(fc, diff_opts); + else + error = diff_file_content_load_blob(fc, diff_opts); + if (error) + return error; + + fc->flags |= GIT_DIFF_FLAG__LOADED; + + diff_file_content_binary_by_content(fc); + + return 0; +} + +void git_diff_file_content__unload(git_diff_file_content *fc) +{ + if ((fc->flags & GIT_DIFF_FLAG__LOADED) == 0) + return; + + if (fc->flags & GIT_DIFF_FLAG__FREE_DATA) { + git__free(fc->map.data); + fc->map.data = ""; + fc->map.len = 0; + fc->flags &= ~GIT_DIFF_FLAG__FREE_DATA; + } + else if (fc->flags & GIT_DIFF_FLAG__UNMAP_DATA) { + git_futils_mmap_free(&fc->map); + fc->map.data = ""; + fc->map.len = 0; + fc->flags &= ~GIT_DIFF_FLAG__UNMAP_DATA; + } + + if (fc->flags & GIT_DIFF_FLAG__FREE_BLOB) { + git_blob_free((git_blob *)fc->blob); + fc->blob = NULL; + fc->flags &= ~GIT_DIFF_FLAG__FREE_BLOB; + } + + fc->flags &= ~GIT_DIFF_FLAG__LOADED; +} + +void git_diff_file_content__clear(git_diff_file_content *fc) +{ + git_diff_file_content__unload(fc); + + /* for now, nothing else to do */ +} diff --git a/src/libgit2/diff_file.h b/src/libgit2/diff_file.h new file mode 100644 index 0000000..8d743e8 --- /dev/null +++ b/src/libgit2/diff_file.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_diff_file_h__ +#define INCLUDE_diff_file_h__ + +#include "common.h" + +#include "diff.h" +#include "diff_driver.h" +#include "map.h" + +/* expanded information for one side of a delta */ +typedef struct { + git_repository *repo; + git_diff_file *file; + git_diff_driver *driver; + uint32_t flags; + uint32_t opts_flags; + git_object_size_t opts_max_size; + git_iterator_t src; + const git_blob *blob; + git_map map; +} git_diff_file_content; + +extern int git_diff_file_content__init_from_diff( + git_diff_file_content *fc, + git_diff *diff, + git_diff_delta *delta, + bool use_old); + +typedef struct { + const git_blob *blob; + const void *buf; + size_t buflen; + const char *as_path; +} git_diff_file_content_src; + +#define GIT_DIFF_FILE_CONTENT_SRC__BLOB(BLOB,PATH) { (BLOB),NULL,0,(PATH) } +#define GIT_DIFF_FILE_CONTENT_SRC__BUF(BUF,LEN,PATH) { NULL,(BUF),(LEN),(PATH) } + +extern int git_diff_file_content__init_from_src( + git_diff_file_content *fc, + git_repository *repo, + const git_diff_options *opts, + const git_diff_file_content_src *src, + git_diff_file *as_file); + +/* this loads the blob/file-on-disk as needed */ +extern int git_diff_file_content__load( + git_diff_file_content *fc, + git_diff_options *diff_opts); + +/* this releases the blob/file-in-memory */ +extern void git_diff_file_content__unload(git_diff_file_content *fc); + +/* this unloads and also releases any other resources */ +extern void git_diff_file_content__clear(git_diff_file_content *fc); + +#endif diff --git a/src/libgit2/diff_generate.c b/src/libgit2/diff_generate.c new file mode 100644 index 0000000..78fe510 --- /dev/null +++ b/src/libgit2/diff_generate.c @@ -0,0 +1,1750 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "diff_generate.h" + +#include "diff.h" +#include "patch_generate.h" +#include "futils.h" +#include "config.h" +#include "attr_file.h" +#include "filter.h" +#include "pathspec.h" +#include "index.h" +#include "odb.h" +#include "submodule.h" + +#define DIFF_FLAG_IS_SET(DIFF,FLAG) \ + (((DIFF)->base.opts.flags & (FLAG)) != 0) +#define DIFF_FLAG_ISNT_SET(DIFF,FLAG) \ + (((DIFF)->base.opts.flags & (FLAG)) == 0) +#define DIFF_FLAG_SET(DIFF,FLAG,VAL) (DIFF)->base.opts.flags = \ + (VAL) ? ((DIFF)->base.opts.flags | (FLAG)) : \ + ((DIFF)->base.opts.flags & ~(FLAG)) + +typedef struct { + struct git_diff base; + + git_vector pathspec; + + uint32_t diffcaps; + bool index_updated; +} git_diff_generated; + +static git_diff_delta *diff_delta__alloc( + git_diff_generated *diff, + git_delta_t status, + const char *path) +{ + git_diff_delta *delta = git__calloc(1, sizeof(git_diff_delta)); + if (!delta) + return NULL; + + delta->old_file.path = git_pool_strdup(&diff->base.pool, path); + if (delta->old_file.path == NULL) { + git__free(delta); + return NULL; + } + + delta->new_file.path = delta->old_file.path; + + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { + switch (status) { + case GIT_DELTA_ADDED: status = GIT_DELTA_DELETED; break; + case GIT_DELTA_DELETED: status = GIT_DELTA_ADDED; break; + default: break; /* leave other status values alone */ + } + } + delta->status = status; + + git_oid_clear(&delta->old_file.id, diff->base.opts.oid_type); + git_oid_clear(&delta->new_file.id, diff->base.opts.oid_type); + + return delta; +} + +static int diff_insert_delta( + git_diff_generated *diff, + git_diff_delta *delta, + const char *matched_pathspec) +{ + int error = 0; + + if (diff->base.opts.notify_cb) { + error = diff->base.opts.notify_cb( + &diff->base, delta, matched_pathspec, diff->base.opts.payload); + + if (error) { + git__free(delta); + + if (error > 0) /* positive value means to skip this delta */ + return 0; + else /* negative value means to cancel diff */ + return git_error_set_after_callback_function(error, "git_diff"); + } + } + + if ((error = git_vector_insert(&diff->base.deltas, delta)) < 0) + git__free(delta); + + return error; +} + +static bool diff_pathspec_match( + const char **matched_pathspec, + git_diff_generated *diff, + const git_index_entry *entry) +{ + bool disable_pathspec_match = + DIFF_FLAG_IS_SET(diff, GIT_DIFF_DISABLE_PATHSPEC_MATCH); + + /* If we're disabling fnmatch, then the iterator has already applied + * the filters to the files for us and we don't have to do anything. + * However, this only applies to *files* - the iterator will include + * directories that we need to recurse into when not autoexpanding, + * so we still need to apply the pathspec match to directories. + */ + if ((S_ISLNK(entry->mode) || S_ISREG(entry->mode)) && + disable_pathspec_match) { + *matched_pathspec = entry->path; + return true; + } + + return git_pathspec__match( + &diff->pathspec, entry->path, disable_pathspec_match, + DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE), + matched_pathspec, NULL); +} + +static void diff_delta__flag_known_size(git_diff_file *file) +{ + /* + * If we don't know the ID, that can only come from the workdir + * iterator, which means we *do* know the file size. This is a + * leaky abstraction, but alas. Otherwise, we test against the + * empty blob id. + */ + if (file->size || + !(file->flags & GIT_DIFF_FLAG_VALID_ID) || + git_oid_equal(&file->id, &git_oid__empty_blob_sha1)) + file->flags |= GIT_DIFF_FLAG_VALID_SIZE; +} + +static void diff_delta__flag_known_sizes(git_diff_delta *delta) +{ + diff_delta__flag_known_size(&delta->old_file); + diff_delta__flag_known_size(&delta->new_file); +} + +static int diff_delta__from_one( + git_diff_generated *diff, + git_delta_t status, + const git_index_entry *oitem, + const git_index_entry *nitem) +{ + const git_index_entry *entry = nitem; + bool has_old = false; + git_diff_delta *delta; + git_oid_t oid_type; + const char *matched_pathspec; + + GIT_ASSERT_ARG((oitem != NULL) ^ (nitem != NULL)); + + oid_type = diff->base.opts.oid_type; + + if (oitem) { + entry = oitem; + has_old = true; + } + + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) + has_old = !has_old; + + if ((entry->flags & GIT_INDEX_ENTRY_VALID) != 0) + return 0; + + if (status == GIT_DELTA_IGNORED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_IGNORED)) + return 0; + + if (status == GIT_DELTA_UNTRACKED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNTRACKED)) + return 0; + + if (status == GIT_DELTA_UNREADABLE && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNREADABLE)) + return 0; + + if (!diff_pathspec_match(&matched_pathspec, diff, entry)) + return 0; + + delta = diff_delta__alloc(diff, status, entry->path); + GIT_ERROR_CHECK_ALLOC(delta); + + /* This fn is just for single-sided diffs */ + GIT_ASSERT(status != GIT_DELTA_MODIFIED); + delta->nfiles = 1; + + git_oid_clear(&delta->old_file.id, diff->base.opts.oid_type); + git_oid_clear(&delta->new_file.id, diff->base.opts.oid_type); + + if (has_old) { + delta->old_file.mode = entry->mode; + delta->old_file.size = entry->file_size; + delta->old_file.flags |= GIT_DIFF_FLAG_EXISTS; + git_oid_cpy(&delta->old_file.id, &entry->id); + git_oid_clear(&delta->new_file.id, oid_type); + delta->old_file.id_abbrev = (uint16_t)git_oid_hexsize(oid_type); + } else /* ADDED, IGNORED, UNTRACKED */ { + delta->new_file.mode = entry->mode; + delta->new_file.size = entry->file_size; + delta->new_file.flags |= GIT_DIFF_FLAG_EXISTS; + git_oid_clear(&delta->old_file.id, oid_type); + git_oid_cpy(&delta->new_file.id, &entry->id); + delta->new_file.id_abbrev = (uint16_t)git_oid_hexsize(oid_type); + } + + delta->old_file.flags |= GIT_DIFF_FLAG_VALID_ID; + + if (has_old || !git_oid_is_zero(&delta->new_file.id)) + delta->new_file.flags |= GIT_DIFF_FLAG_VALID_ID; + + diff_delta__flag_known_sizes(delta); + + return diff_insert_delta(diff, delta, matched_pathspec); +} + +static int diff_delta__from_two( + git_diff_generated *diff, + git_delta_t status, + const git_index_entry *old_entry, + uint32_t old_mode, + const git_index_entry *new_entry, + uint32_t new_mode, + const git_oid *new_id, + const char *matched_pathspec) +{ + const git_oid *old_id = &old_entry->id; + git_diff_delta *delta; + const char *canonical_path = old_entry->path; + git_oid_t oid_type; + + oid_type = diff->base.opts.oid_type; + + if (status == GIT_DELTA_UNMODIFIED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNMODIFIED)) + return 0; + + if (!new_id) + new_id = &new_entry->id; + + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { + uint32_t temp_mode = old_mode; + const git_index_entry *temp_entry = old_entry; + const git_oid *temp_id = old_id; + + old_entry = new_entry; + new_entry = temp_entry; + old_mode = new_mode; + new_mode = temp_mode; + old_id = new_id; + new_id = temp_id; + } + + delta = diff_delta__alloc(diff, status, canonical_path); + GIT_ERROR_CHECK_ALLOC(delta); + delta->nfiles = 2; + + if (!git_index_entry_is_conflict(old_entry)) { + delta->old_file.size = old_entry->file_size; + delta->old_file.mode = old_mode; + git_oid_cpy(&delta->old_file.id, old_id); + delta->old_file.id_abbrev = (uint16_t)git_oid_hexsize(oid_type); + delta->old_file.flags |= GIT_DIFF_FLAG_VALID_ID | + GIT_DIFF_FLAG_EXISTS; + } + + if (!git_index_entry_is_conflict(new_entry)) { + git_oid_cpy(&delta->new_file.id, new_id); + delta->new_file.id_abbrev = (uint16_t)git_oid_hexsize(oid_type); + delta->new_file.size = new_entry->file_size; + delta->new_file.mode = new_mode; + delta->old_file.flags |= GIT_DIFF_FLAG_EXISTS; + delta->new_file.flags |= GIT_DIFF_FLAG_EXISTS; + + if (!git_oid_is_zero(&new_entry->id)) + delta->new_file.flags |= GIT_DIFF_FLAG_VALID_ID; + } + + diff_delta__flag_known_sizes(delta); + + return diff_insert_delta(diff, delta, matched_pathspec); +} + +static git_diff_delta *diff_delta__last_for_item( + git_diff_generated *diff, + const git_index_entry *item) +{ + git_diff_delta *delta = git_vector_last(&diff->base.deltas); + if (!delta) + return NULL; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + case GIT_DELTA_DELETED: + if (git_oid__cmp(&delta->old_file.id, &item->id) == 0) + return delta; + break; + case GIT_DELTA_ADDED: + if (git_oid__cmp(&delta->new_file.id, &item->id) == 0) + return delta; + break; + case GIT_DELTA_UNREADABLE: + case GIT_DELTA_UNTRACKED: + if (diff->base.strcomp(delta->new_file.path, item->path) == 0 && + git_oid__cmp(&delta->new_file.id, &item->id) == 0) + return delta; + break; + case GIT_DELTA_MODIFIED: + if (git_oid__cmp(&delta->old_file.id, &item->id) == 0 || + (delta->new_file.mode == item->mode && + git_oid__cmp(&delta->new_file.id, &item->id) == 0)) + return delta; + break; + default: + break; + } + + return NULL; +} + +static char *diff_strdup_prefix(git_pool *pool, const char *prefix) +{ + size_t len = strlen(prefix); + + /* append '/' at end if needed */ + if (len > 0 && prefix[len - 1] != '/') + return git_pool_strcat(pool, prefix, "/"); + else + return git_pool_strndup(pool, prefix, len + 1); +} + +GIT_INLINE(const char *) diff_delta__i2w_path(const git_diff_delta *delta) +{ + return delta->old_file.path ? + delta->old_file.path : delta->new_file.path; +} + +static int diff_delta_i2w_cmp(const void *a, const void *b) +{ + const git_diff_delta *da = a, *db = b; + int val = strcmp(diff_delta__i2w_path(da), diff_delta__i2w_path(db)); + return val ? val : ((int)da->status - (int)db->status); +} + +static int diff_delta_i2w_casecmp(const void *a, const void *b) +{ + const git_diff_delta *da = a, *db = b; + int val = strcasecmp(diff_delta__i2w_path(da), diff_delta__i2w_path(db)); + return val ? val : ((int)da->status - (int)db->status); +} + +bool git_diff_delta__should_skip( + const git_diff_options *opts, const git_diff_delta *delta) +{ + uint32_t flags = opts ? opts->flags : 0; + + if (delta->status == GIT_DELTA_UNMODIFIED && + (flags & GIT_DIFF_INCLUDE_UNMODIFIED) == 0) + return true; + + if (delta->status == GIT_DELTA_IGNORED && + (flags & GIT_DIFF_INCLUDE_IGNORED) == 0) + return true; + + if (delta->status == GIT_DELTA_UNTRACKED && + (flags & GIT_DIFF_INCLUDE_UNTRACKED) == 0) + return true; + + if (delta->status == GIT_DELTA_UNREADABLE && + (flags & GIT_DIFF_INCLUDE_UNREADABLE) == 0) + return true; + + return false; +} + + +static const char *diff_mnemonic_prefix( + git_iterator_t type, bool left_side) +{ + const char *pfx = ""; + + switch (type) { + case GIT_ITERATOR_EMPTY: pfx = "c"; break; + case GIT_ITERATOR_TREE: pfx = "c"; break; + case GIT_ITERATOR_INDEX: pfx = "i"; break; + case GIT_ITERATOR_WORKDIR: pfx = "w"; break; + case GIT_ITERATOR_FS: pfx = left_side ? "1" : "2"; break; + default: break; + } + + /* note: without a deeper look at pathspecs, there is no easy way + * to get the (o)bject / (w)ork tree mnemonics working... + */ + + return pfx; +} + +static void diff_set_ignore_case(git_diff *diff, bool ignore_case) +{ + if (!ignore_case) { + diff->opts.flags &= ~GIT_DIFF_IGNORE_CASE; + + diff->strcomp = git__strcmp; + diff->strncomp = git__strncmp; + diff->pfxcomp = git__prefixcmp; + diff->entrycomp = git_diff__entry_cmp; + + git_vector_set_cmp(&diff->deltas, git_diff_delta__cmp); + } else { + diff->opts.flags |= GIT_DIFF_IGNORE_CASE; + + diff->strcomp = git__strcasecmp; + diff->strncomp = git__strncasecmp; + diff->pfxcomp = git__prefixcmp_icase; + diff->entrycomp = git_diff__entry_icmp; + + git_vector_set_cmp(&diff->deltas, git_diff_delta__casecmp); + } + + git_vector_sort(&diff->deltas); +} + +static void diff_generated_free(git_diff *d) +{ + git_diff_generated *diff = (git_diff_generated *)d; + + git_attr_session__free(&diff->base.attrsession); + git_vector_free_deep(&diff->base.deltas); + + git_pathspec__vfree(&diff->pathspec); + git_pool_clear(&diff->base.pool); + + git__memzero(diff, sizeof(*diff)); + git__free(diff); +} + +static git_diff_generated *diff_generated_alloc( + git_repository *repo, + git_iterator *old_iter, + git_iterator *new_iter) +{ + git_diff_generated *diff; + git_diff_options dflt = GIT_DIFF_OPTIONS_INIT; + + GIT_ASSERT_ARG_WITH_RETVAL(repo, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(old_iter, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(new_iter, NULL); + + if ((diff = git__calloc(1, sizeof(git_diff_generated))) == NULL) + return NULL; + + GIT_REFCOUNT_INC(&diff->base); + diff->base.type = GIT_DIFF_TYPE_GENERATED; + diff->base.repo = repo; + diff->base.old_src = old_iter->type; + diff->base.new_src = new_iter->type; + diff->base.patch_fn = git_patch_generated_from_diff; + diff->base.free_fn = diff_generated_free; + git_attr_session__init(&diff->base.attrsession, repo); + memcpy(&diff->base.opts, &dflt, sizeof(git_diff_options)); + + if (git_pool_init(&diff->base.pool, 1) < 0 || + git_vector_init(&diff->base.deltas, 0, git_diff_delta__cmp) < 0) { + git_diff_free(&diff->base); + return NULL; + } + + /* Use case-insensitive compare if either iterator has + * the ignore_case bit set */ + diff_set_ignore_case( + &diff->base, + git_iterator_ignore_case(old_iter) || + git_iterator_ignore_case(new_iter)); + + return diff; +} + +static int diff_generated_apply_options( + git_diff_generated *diff, + const git_diff_options *opts) +{ + git_config *cfg = NULL; + git_repository *repo = diff->base.repo; + git_pool *pool = &diff->base.pool; + int val; + + if (opts) { + /* copy user options (except case sensitivity info from iterators) */ + bool icase = DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE); + memcpy(&diff->base.opts, opts, sizeof(diff->base.opts)); + DIFF_FLAG_SET(diff, GIT_DIFF_IGNORE_CASE, icase); + + /* initialize pathspec from options */ + if (git_pathspec__vinit(&diff->pathspec, &opts->pathspec, pool) < 0) + return -1; + } + + if (!diff->base.opts.oid_type) { + diff->base.opts.oid_type = repo->oid_type; + } else if (diff->base.opts.oid_type != repo->oid_type) { + git_error_set(GIT_ERROR_INVALID, + "specified object ID type does not match repository object ID type"); + return -1; + } + + /* flag INCLUDE_TYPECHANGE_TREES implies INCLUDE_TYPECHANGE */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES)) + diff->base.opts.flags |= GIT_DIFF_INCLUDE_TYPECHANGE; + + /* flag INCLUDE_UNTRACKED_CONTENT implies INCLUDE_UNTRACKED */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_SHOW_UNTRACKED_CONTENT)) + diff->base.opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED; + + /* load config values that affect diff behavior */ + if ((val = git_repository_config_snapshot(&cfg, repo)) < 0) + return val; + + if (!git_config__configmap_lookup(&val, cfg, GIT_CONFIGMAP_SYMLINKS) && val) + diff->diffcaps |= GIT_DIFFCAPS_HAS_SYMLINKS; + + if (!git_config__configmap_lookup(&val, cfg, GIT_CONFIGMAP_IGNORESTAT) && val) + diff->diffcaps |= GIT_DIFFCAPS_IGNORE_STAT; + + if ((diff->base.opts.flags & GIT_DIFF_IGNORE_FILEMODE) == 0 && + !git_config__configmap_lookup(&val, cfg, GIT_CONFIGMAP_FILEMODE) && val) + diff->diffcaps |= GIT_DIFFCAPS_TRUST_MODE_BITS; + + if (!git_config__configmap_lookup(&val, cfg, GIT_CONFIGMAP_TRUSTCTIME) && val) + diff->diffcaps |= GIT_DIFFCAPS_TRUST_CTIME; + + /* Don't set GIT_DIFFCAPS_USE_DEV - compile time option in core git */ + + /* If not given explicit `opts`, check `diff.xyz` configs */ + if (!opts) { + int context = git_config__get_int_force(cfg, "diff.context", 3); + diff->base.opts.context_lines = context >= 0 ? (uint32_t)context : 3; + + /* add other defaults here */ + } + + /* Reverse src info if diff is reversed */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { + git_iterator_t tmp_src = diff->base.old_src; + diff->base.old_src = diff->base.new_src; + diff->base.new_src = tmp_src; + } + + /* Unset UPDATE_INDEX unless diffing workdir and index */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_UPDATE_INDEX) && + (!(diff->base.old_src == GIT_ITERATOR_WORKDIR || + diff->base.new_src == GIT_ITERATOR_WORKDIR) || + !(diff->base.old_src == GIT_ITERATOR_INDEX || + diff->base.new_src == GIT_ITERATOR_INDEX))) + diff->base.opts.flags &= ~GIT_DIFF_UPDATE_INDEX; + + /* if ignore_submodules not explicitly set, check diff config */ + if (diff->base.opts.ignore_submodules <= 0) { + git_config_entry *entry; + git_config__lookup_entry(&entry, cfg, "diff.ignoresubmodules", true); + + if (entry && git_submodule_parse_ignore( + &diff->base.opts.ignore_submodules, entry->value) < 0) + git_error_clear(); + git_config_entry_free(entry); + } + + /* if either prefix is not set, figure out appropriate value */ + if (!diff->base.opts.old_prefix || !diff->base.opts.new_prefix) { + const char *use_old = DIFF_OLD_PREFIX_DEFAULT; + const char *use_new = DIFF_NEW_PREFIX_DEFAULT; + + if (git_config__get_bool_force(cfg, "diff.noprefix", 0)) + use_old = use_new = ""; + else if (git_config__get_bool_force(cfg, "diff.mnemonicprefix", 0)) { + use_old = diff_mnemonic_prefix(diff->base.old_src, true); + use_new = diff_mnemonic_prefix(diff->base.new_src, false); + } + + if (!diff->base.opts.old_prefix) + diff->base.opts.old_prefix = use_old; + if (!diff->base.opts.new_prefix) + diff->base.opts.new_prefix = use_new; + } + + /* strdup prefix from pool so we're not dependent on external data */ + diff->base.opts.old_prefix = diff_strdup_prefix(pool, diff->base.opts.old_prefix); + diff->base.opts.new_prefix = diff_strdup_prefix(pool, diff->base.opts.new_prefix); + + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { + const char *tmp_prefix = diff->base.opts.old_prefix; + diff->base.opts.old_prefix = diff->base.opts.new_prefix; + diff->base.opts.new_prefix = tmp_prefix; + } + + git_config_free(cfg); + + /* check strdup results for error */ + return (!diff->base.opts.old_prefix || !diff->base.opts.new_prefix) ? -1 : 0; +} + +int git_diff__oid_for_file( + git_oid *out, + git_diff *diff, + const char *path, + uint16_t mode, + git_object_size_t size) +{ + git_index_entry entry; + + if (size > UINT32_MAX) { + git_error_set(GIT_ERROR_NOMEMORY, "file size overflow (for 32-bits) on '%s'", path); + return -1; + } + + memset(&entry, 0, sizeof(entry)); + entry.mode = mode; + entry.file_size = (uint32_t)size; + entry.path = (char *)path; + git_oid_clear(&entry.id, diff->opts.oid_type); + + return git_diff__oid_for_entry(out, diff, &entry, mode, NULL); +} + +int git_diff__oid_for_entry( + git_oid *out, + git_diff *d, + const git_index_entry *src, + uint16_t mode, + const git_oid *update_match) +{ + git_diff_generated *diff; + git_str full_path = GIT_STR_INIT; + git_index_entry entry = *src; + git_filter_list *fl = NULL; + int error = 0; + + GIT_ASSERT(d->type == GIT_DIFF_TYPE_GENERATED); + diff = (git_diff_generated *)d; + + git_oid_clear(out, diff->base.opts.oid_type); + + if (git_repository_workdir_path(&full_path, diff->base.repo, entry.path) < 0) + return -1; + + if (!mode) { + struct stat st; + + diff->base.perf.stat_calls++; + + if (p_stat(full_path.ptr, &st) < 0) { + error = git_fs_path_set_error(errno, entry.path, "stat"); + git_str_dispose(&full_path); + return error; + } + + git_index_entry__init_from_stat(&entry, + &st, (diff->diffcaps & GIT_DIFFCAPS_TRUST_MODE_BITS) != 0); + } + + /* calculate OID for file if possible */ + if (S_ISGITLINK(mode)) { + git_submodule *sm; + + if (!git_submodule_lookup(&sm, diff->base.repo, entry.path)) { + const git_oid *sm_oid = git_submodule_wd_id(sm); + if (sm_oid) + git_oid_cpy(out, sm_oid); + git_submodule_free(sm); + } else { + /* if submodule lookup failed probably just in an intermediate + * state where some init hasn't happened, so ignore the error + */ + git_error_clear(); + } + } else if (S_ISLNK(mode)) { + error = git_odb__hashlink(out, full_path.ptr, + diff->base.opts.oid_type); + diff->base.perf.oid_calculations++; + } else if (!git__is_sizet(entry.file_size)) { + git_error_set(GIT_ERROR_NOMEMORY, "file size overflow (for 32-bits) on '%s'", + entry.path); + error = -1; + } else if (!(error = git_filter_list_load(&fl, + diff->base.repo, NULL, entry.path, + GIT_FILTER_TO_ODB, GIT_FILTER_ALLOW_UNSAFE))) + { + int fd = git_futils_open_ro(full_path.ptr); + if (fd < 0) + error = fd; + else { + error = git_odb__hashfd_filtered( + out, fd, (size_t)entry.file_size, + GIT_OBJECT_BLOB, diff->base.opts.oid_type, + fl); + p_close(fd); + diff->base.perf.oid_calculations++; + } + + git_filter_list_free(fl); + } + + /* update index for entry if requested */ + if (!error && update_match && git_oid_equal(out, update_match)) { + git_index *idx; + git_index_entry updated_entry; + + memcpy(&updated_entry, &entry, sizeof(git_index_entry)); + updated_entry.mode = mode; + git_oid_cpy(&updated_entry.id, out); + + if (!(error = git_repository_index__weakptr(&idx, + diff->base.repo))) { + error = git_index_add(idx, &updated_entry); + diff->index_updated = true; + } + } + + git_str_dispose(&full_path); + return error; +} + +typedef struct { + git_repository *repo; + git_iterator *old_iter; + git_iterator *new_iter; + const git_index_entry *oitem; + const git_index_entry *nitem; + git_strmap *submodule_cache; + bool submodule_cache_initialized; +} diff_in_progress; + +#define MODE_BITS_MASK 0000777 + +static int maybe_modified_submodule( + git_delta_t *status, + git_oid *found_oid, + git_diff_generated *diff, + diff_in_progress *info) +{ + int error = 0; + git_submodule *sub; + unsigned int sm_status = 0; + git_submodule_ignore_t ign = diff->base.opts.ignore_submodules; + git_strmap *submodule_cache = NULL; + + *status = GIT_DELTA_UNMODIFIED; + + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES) || + ign == GIT_SUBMODULE_IGNORE_ALL) + return 0; + + if (diff->base.repo->submodule_cache != NULL) { + submodule_cache = diff->base.repo->submodule_cache; + } else { + if (!info->submodule_cache_initialized) { + info->submodule_cache_initialized = true; + /* + * Try to cache the submodule information to avoid having to parse it for + * every submodule. It is okay if it fails, the cache will still be NULL + * and the submodules will be attempted to be looked up individually. + */ + git_submodule_cache_init(&info->submodule_cache, diff->base.repo); + } + submodule_cache = info->submodule_cache; + } + + if ((error = git_submodule__lookup_with_cache( + &sub, diff->base.repo, info->nitem->path, submodule_cache)) < 0) { + + /* GIT_EEXISTS means dir with .git in it was found - ignore it */ + if (error == GIT_EEXISTS) { + git_error_clear(); + error = 0; + } + return error; + } + + if (ign <= 0 && git_submodule_ignore(sub) == GIT_SUBMODULE_IGNORE_ALL) + /* ignore it */; + else if ((error = git_submodule__status( + &sm_status, NULL, NULL, found_oid, sub, ign)) < 0) + /* return error below */; + + /* check IS_WD_UNMODIFIED because this case is only used + * when the new side of the diff is the working directory + */ + else if (!GIT_SUBMODULE_STATUS_IS_WD_UNMODIFIED(sm_status)) + *status = GIT_DELTA_MODIFIED; + + /* now that we have a HEAD OID, check if HEAD moved */ + else if ((sm_status & GIT_SUBMODULE_STATUS_IN_WD) != 0 && + !git_oid_equal(&info->oitem->id, found_oid)) + *status = GIT_DELTA_MODIFIED; + + git_submodule_free(sub); + return error; +} + +static int maybe_modified( + git_diff_generated *diff, + diff_in_progress *info) +{ + git_oid noid; + git_delta_t status = GIT_DELTA_MODIFIED; + const git_index_entry *oitem = info->oitem; + const git_index_entry *nitem = info->nitem; + unsigned int omode = oitem->mode; + unsigned int nmode = nitem->mode; + bool new_is_workdir = (info->new_iter->type == GIT_ITERATOR_WORKDIR); + bool modified_uncertain = false; + const char *matched_pathspec; + int error = 0; + + git_oid_clear(&noid, diff->base.opts.oid_type); + + if (!diff_pathspec_match(&matched_pathspec, diff, oitem)) + return 0; + + /* on platforms with no symlinks, preserve mode of existing symlinks */ + if (S_ISLNK(omode) && S_ISREG(nmode) && new_is_workdir && + !(diff->diffcaps & GIT_DIFFCAPS_HAS_SYMLINKS)) + nmode = omode; + + /* on platforms with no execmode, just preserve old mode */ + if (!(diff->diffcaps & GIT_DIFFCAPS_TRUST_MODE_BITS) && + (nmode & MODE_BITS_MASK) != (omode & MODE_BITS_MASK) && + new_is_workdir) + nmode = (nmode & ~MODE_BITS_MASK) | (omode & MODE_BITS_MASK); + + /* if one side is a conflict, mark the whole delta as conflicted */ + if (git_index_entry_is_conflict(oitem) || + git_index_entry_is_conflict(nitem)) { + status = GIT_DELTA_CONFLICTED; + + /* support "assume unchanged" (poorly, b/c we still stat everything) */ + } else if ((oitem->flags & GIT_INDEX_ENTRY_VALID) != 0) { + status = GIT_DELTA_UNMODIFIED; + + /* support "skip worktree" index bit */ + } else if ((oitem->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE) != 0) { + status = GIT_DELTA_UNMODIFIED; + + /* if basic type of file changed, then split into delete and add */ + } else if (GIT_MODE_TYPE(omode) != GIT_MODE_TYPE(nmode)) { + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE)) { + status = GIT_DELTA_TYPECHANGE; + } + + else if (nmode == GIT_FILEMODE_UNREADABLE) { + if (!(error = diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem, NULL))) + error = diff_delta__from_one(diff, GIT_DELTA_UNREADABLE, NULL, nitem); + return error; + } + + else { + if (!(error = diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem, NULL))) + error = diff_delta__from_one(diff, GIT_DELTA_ADDED, NULL, nitem); + return error; + } + + /* if oids and modes match (and are valid), then file is unmodified */ + } else if (git_oid_equal(&oitem->id, &nitem->id) && + omode == nmode && + !git_oid_is_zero(&oitem->id)) { + status = GIT_DELTA_UNMODIFIED; + + /* if we have an unknown OID and a workdir iterator, then check some + * circumstances that can accelerate things or need special handling + */ + } else if (git_oid_is_zero(&nitem->id) && new_is_workdir) { + bool use_ctime = + ((diff->diffcaps & GIT_DIFFCAPS_TRUST_CTIME) != 0); + git_index *index = git_iterator_index(info->new_iter); + + status = GIT_DELTA_UNMODIFIED; + + if (S_ISGITLINK(nmode)) { + if ((error = maybe_modified_submodule(&status, &noid, diff, info)) < 0) + return error; + } + + /* if the stat data looks different, then mark modified - this just + * means that the OID will be recalculated below to confirm change + */ + else if (omode != nmode || oitem->file_size != nitem->file_size) { + status = GIT_DELTA_MODIFIED; + modified_uncertain = + (oitem->file_size <= 0 && nitem->file_size > 0); + } + else if (!git_index_time_eq(&oitem->mtime, &nitem->mtime) || + (use_ctime && !git_index_time_eq(&oitem->ctime, &nitem->ctime)) || + oitem->ino != nitem->ino || + oitem->uid != nitem->uid || + oitem->gid != nitem->gid || + git_index_entry_newer_than_index(nitem, index)) + { + status = GIT_DELTA_MODIFIED; + modified_uncertain = true; + } + + /* if mode is GITLINK and submodules are ignored, then skip */ + } else if (S_ISGITLINK(nmode) && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) { + status = GIT_DELTA_UNMODIFIED; + } + + /* if we got here and decided that the files are modified, but we + * haven't calculated the OID of the new item, then calculate it now + */ + if (modified_uncertain && git_oid_is_zero(&nitem->id)) { + const git_oid *update_check = + DIFF_FLAG_IS_SET(diff, GIT_DIFF_UPDATE_INDEX) && omode == nmode ? + &oitem->id : NULL; + + if ((error = git_diff__oid_for_entry( + &noid, &diff->base, nitem, nmode, update_check)) < 0) + return error; + + /* if oid matches, then mark unmodified (except submodules, where + * the filesystem content may be modified even if the oid still + * matches between the index and the workdir HEAD) + */ + if (omode == nmode && !S_ISGITLINK(omode) && + git_oid_equal(&oitem->id, &noid)) + status = GIT_DELTA_UNMODIFIED; + } + + /* If we want case changes, then break this into a delete of the old + * and an add of the new so that consumers can act accordingly (eg, + * checkout will update the case on disk.) + */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE) && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_CASECHANGE) && + strcmp(oitem->path, nitem->path) != 0) { + + if (!(error = diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem, NULL))) + error = diff_delta__from_one(diff, GIT_DELTA_ADDED, NULL, nitem); + + return error; + } + + return diff_delta__from_two( + diff, status, oitem, omode, nitem, nmode, + git_oid_is_zero(&noid) ? NULL : &noid, matched_pathspec); +} + +static bool entry_is_prefixed( + git_diff_generated *diff, + const git_index_entry *item, + const git_index_entry *prefix_item) +{ + size_t pathlen; + + if (!item || diff->base.pfxcomp(item->path, prefix_item->path) != 0) + return false; + + pathlen = strlen(prefix_item->path); + + return (prefix_item->path[pathlen - 1] == '/' || + item->path[pathlen] == '\0' || + item->path[pathlen] == '/'); +} + +static int iterator_current( + const git_index_entry **entry, + git_iterator *iterator) +{ + int error; + + if ((error = git_iterator_current(entry, iterator)) == GIT_ITEROVER) { + *entry = NULL; + error = 0; + } + + return error; +} + +static int iterator_advance( + const git_index_entry **entry, + git_iterator *iterator) +{ + const git_index_entry *prev_entry = *entry; + int cmp, error; + + /* if we're looking for conflicts, we only want to report + * one conflict for each file, instead of all three sides. + * so if this entry is a conflict for this file, and the + * previous one was a conflict for the same file, skip it. + */ + while ((error = git_iterator_advance(entry, iterator)) == 0) { + if (!(iterator->flags & GIT_ITERATOR_INCLUDE_CONFLICTS) || + !git_index_entry_is_conflict(prev_entry) || + !git_index_entry_is_conflict(*entry)) + break; + + cmp = (iterator->flags & GIT_ITERATOR_IGNORE_CASE) ? + strcasecmp(prev_entry->path, (*entry)->path) : + strcmp(prev_entry->path, (*entry)->path); + + if (cmp) + break; + } + + if (error == GIT_ITEROVER) { + *entry = NULL; + error = 0; + } + + return error; +} + +static int iterator_advance_into( + const git_index_entry **entry, + git_iterator *iterator) +{ + int error; + + if ((error = git_iterator_advance_into(entry, iterator)) == GIT_ITEROVER) { + *entry = NULL; + error = 0; + } + + return error; +} + +static int iterator_advance_over( + const git_index_entry **entry, + git_iterator_status_t *status, + git_iterator *iterator) +{ + int error = git_iterator_advance_over(entry, status, iterator); + + if (error == GIT_ITEROVER) { + *entry = NULL; + error = 0; + } + + return error; +} + +static int handle_unmatched_new_item( + git_diff_generated *diff, diff_in_progress *info) +{ + int error = 0; + const git_index_entry *nitem = info->nitem; + git_delta_t delta_type = GIT_DELTA_UNTRACKED; + bool contains_oitem; + + /* check if this is a prefix of the other side */ + contains_oitem = entry_is_prefixed(diff, info->oitem, nitem); + + /* update delta_type if this item is conflicted */ + if (git_index_entry_is_conflict(nitem)) + delta_type = GIT_DELTA_CONFLICTED; + + /* update delta_type if this item is ignored */ + else if (git_iterator_current_is_ignored(info->new_iter)) + delta_type = GIT_DELTA_IGNORED; + + if (nitem->mode == GIT_FILEMODE_TREE) { + bool recurse_into_dir = contains_oitem; + + /* check if user requests recursion into this type of dir */ + recurse_into_dir = contains_oitem || + (delta_type == GIT_DELTA_UNTRACKED && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) || + (delta_type == GIT_DELTA_IGNORED && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)); + + /* do not advance into directories that contain a .git file */ + if (recurse_into_dir && !contains_oitem) { + git_str *full = NULL; + if (git_iterator_current_workdir_path(&full, info->new_iter) < 0) + return -1; + if (full && git_fs_path_contains(full, DOT_GIT)) { + /* TODO: warning if not a valid git repository */ + recurse_into_dir = false; + } + } + + /* still have to look into untracked directories to match core git - + * with no untracked files, directory is treated as ignored + */ + if (!recurse_into_dir && + delta_type == GIT_DELTA_UNTRACKED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS)) + { + git_diff_delta *last; + git_iterator_status_t untracked_state; + + /* attempt to insert record for this directory */ + if ((error = diff_delta__from_one(diff, delta_type, NULL, nitem)) != 0) + return error; + + /* if delta wasn't created (because of rules), just skip ahead */ + last = diff_delta__last_for_item(diff, nitem); + if (!last) + return iterator_advance(&info->nitem, info->new_iter); + + /* iterate into dir looking for an actual untracked file */ + if ((error = iterator_advance_over( + &info->nitem, &untracked_state, info->new_iter)) < 0) + return error; + + /* if we found nothing that matched our pathlist filter, exclude */ + if (untracked_state == GIT_ITERATOR_STATUS_FILTERED) { + git_vector_pop(&diff->base.deltas); + git__free(last); + } + + /* if we found nothing or just ignored items, update the record */ + if (untracked_state == GIT_ITERATOR_STATUS_IGNORED || + untracked_state == GIT_ITERATOR_STATUS_EMPTY) { + last->status = GIT_DELTA_IGNORED; + + /* remove the record if we don't want ignored records */ + if (DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_IGNORED)) { + git_vector_pop(&diff->base.deltas); + git__free(last); + } + } + + return 0; + } + + /* try to advance into directory if necessary */ + if (recurse_into_dir) { + error = iterator_advance_into(&info->nitem, info->new_iter); + + /* if directory is empty, can't advance into it, so skip it */ + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = iterator_advance(&info->nitem, info->new_iter); + } + + return error; + } + } + + else if (delta_type == GIT_DELTA_IGNORED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS) && + git_iterator_current_tree_is_ignored(info->new_iter)) + /* item contained in ignored directory, so skip over it */ + return iterator_advance(&info->nitem, info->new_iter); + + else if (info->new_iter->type != GIT_ITERATOR_WORKDIR) { + if (delta_type != GIT_DELTA_CONFLICTED) + delta_type = GIT_DELTA_ADDED; + } + + else if (nitem->mode == GIT_FILEMODE_COMMIT) { + /* ignore things that are not actual submodules */ + if (git_submodule_lookup(NULL, info->repo, nitem->path) != 0) { + git_error_clear(); + delta_type = GIT_DELTA_IGNORED; + + /* if this contains a tracked item, treat as normal TREE */ + if (contains_oitem) { + error = iterator_advance_into(&info->nitem, info->new_iter); + if (error != GIT_ENOTFOUND) + return error; + + git_error_clear(); + return iterator_advance(&info->nitem, info->new_iter); + } + } + } + + else if (nitem->mode == GIT_FILEMODE_UNREADABLE) { + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED)) + delta_type = GIT_DELTA_UNTRACKED; + else + delta_type = GIT_DELTA_UNREADABLE; + } + + /* Actually create the record for this item if necessary */ + if ((error = diff_delta__from_one(diff, delta_type, NULL, nitem)) != 0) + return error; + + /* If user requested TYPECHANGE records, then check for that instead of + * just generating an ADDED/UNTRACKED record + */ + if (delta_type != GIT_DELTA_IGNORED && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && + contains_oitem) + { + /* this entry was prefixed with a tree - make TYPECHANGE */ + git_diff_delta *last = diff_delta__last_for_item(diff, nitem); + if (last) { + last->status = GIT_DELTA_TYPECHANGE; + last->old_file.mode = GIT_FILEMODE_TREE; + } + } + + return iterator_advance(&info->nitem, info->new_iter); +} + +static int handle_unmatched_old_item( + git_diff_generated *diff, diff_in_progress *info) +{ + git_delta_t delta_type = GIT_DELTA_DELETED; + int error; + + /* update delta_type if this item is conflicted */ + if (git_index_entry_is_conflict(info->oitem)) + delta_type = GIT_DELTA_CONFLICTED; + + if ((error = diff_delta__from_one(diff, delta_type, info->oitem, NULL)) < 0) + return error; + + /* if we are generating TYPECHANGE records then check for that + * instead of just generating a DELETE record + */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && + entry_is_prefixed(diff, info->nitem, info->oitem)) + { + /* this entry has become a tree! convert to TYPECHANGE */ + git_diff_delta *last = diff_delta__last_for_item(diff, info->oitem); + if (last) { + last->status = GIT_DELTA_TYPECHANGE; + last->new_file.mode = GIT_FILEMODE_TREE; + } + + /* If new_iter is a workdir iterator, then this situation + * will certainly be followed by a series of untracked items. + * Unless RECURSE_UNTRACKED_DIRS is set, skip over them... + */ + if (S_ISDIR(info->nitem->mode) && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) + return iterator_advance(&info->nitem, info->new_iter); + } + + return iterator_advance(&info->oitem, info->old_iter); +} + +static int handle_matched_item( + git_diff_generated *diff, diff_in_progress *info) +{ + int error = 0; + + if ((error = maybe_modified(diff, info)) < 0) + return error; + + if (!(error = iterator_advance(&info->oitem, info->old_iter))) + error = iterator_advance(&info->nitem, info->new_iter); + + return error; +} + +int git_diff__from_iterators( + git_diff **out, + git_repository *repo, + git_iterator *old_iter, + git_iterator *new_iter, + const git_diff_options *opts) +{ + git_diff_generated *diff; + diff_in_progress info = {0}; + int error = 0; + + *out = NULL; + + diff = diff_generated_alloc(repo, old_iter, new_iter); + GIT_ERROR_CHECK_ALLOC(diff); + + info.repo = repo; + info.old_iter = old_iter; + info.new_iter = new_iter; + + /* make iterators have matching icase behavior */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE)) { + if ((error = git_iterator_set_ignore_case(old_iter, true)) < 0 || + (error = git_iterator_set_ignore_case(new_iter, true)) < 0) + goto cleanup; + } + + /* finish initialization */ + if ((error = diff_generated_apply_options(diff, opts)) < 0) + goto cleanup; + + if ((error = iterator_current(&info.oitem, old_iter)) < 0 || + (error = iterator_current(&info.nitem, new_iter)) < 0) + goto cleanup; + + /* run iterators building diffs */ + while (!error && (info.oitem || info.nitem)) { + int cmp; + + /* report progress */ + if (opts && opts->progress_cb) { + if ((error = opts->progress_cb(&diff->base, + info.oitem ? info.oitem->path : NULL, + info.nitem ? info.nitem->path : NULL, + opts->payload))) + break; + } + + cmp = info.oitem ? + (info.nitem ? diff->base.entrycomp(info.oitem, info.nitem) : -1) : 1; + + /* create DELETED records for old items not matched in new */ + if (cmp < 0) + error = handle_unmatched_old_item(diff, &info); + + /* create ADDED, TRACKED, or IGNORED records for new items not + * matched in old (and/or descend into directories as needed) + */ + else if (cmp > 0) + error = handle_unmatched_new_item(diff, &info); + + /* otherwise item paths match, so create MODIFIED record + * (or ADDED and DELETED pair if type changed) + */ + else + error = handle_matched_item(diff, &info); + } + + diff->base.perf.stat_calls += + old_iter->stat_calls + new_iter->stat_calls; + +cleanup: + if (!error) + *out = &diff->base; + else + git_diff_free(&diff->base); + if (info.submodule_cache) + git_submodule_cache_free(info.submodule_cache); + + return error; +} + +static int diff_prepare_iterator_opts(char **prefix, git_iterator_options *a, int aflags, + git_iterator_options *b, int bflags, + const git_diff_options *opts) +{ + GIT_ERROR_CHECK_VERSION(opts, GIT_DIFF_OPTIONS_VERSION, "git_diff_options"); + + *prefix = NULL; + + if (opts && (opts->flags & GIT_DIFF_DISABLE_PATHSPEC_MATCH)) { + a->pathlist.strings = opts->pathspec.strings; + a->pathlist.count = opts->pathspec.count; + b->pathlist.strings = opts->pathspec.strings; + b->pathlist.count = opts->pathspec.count; + } else if (opts) { + *prefix = git_pathspec_prefix(&opts->pathspec); + GIT_ERROR_CHECK_ALLOC(prefix); + } + + a->flags = aflags; + b->flags = bflags; + a->start = b->start = *prefix; + a->end = b->end = *prefix; + + return 0; +} + +int git_diff_tree_to_tree( + git_diff **out, + git_repository *repo, + git_tree *old_tree, + git_tree *new_tree, + const git_diff_options *opts) +{ + git_iterator_flag_t iflag = GIT_ITERATOR_DONT_IGNORE_CASE; + git_iterator_options a_opts = GIT_ITERATOR_OPTIONS_INIT, + b_opts = GIT_ITERATOR_OPTIONS_INIT; + git_iterator *a = NULL, *b = NULL; + git_diff *diff = NULL; + char *prefix = NULL; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + *out = NULL; + + /* for tree to tree diff, be case sensitive even if the index is + * currently case insensitive, unless the user explicitly asked + * for case insensitivity + */ + if (opts && (opts->flags & GIT_DIFF_IGNORE_CASE) != 0) + iflag = GIT_ITERATOR_IGNORE_CASE; + + if ((error = diff_prepare_iterator_opts(&prefix, &a_opts, iflag, &b_opts, iflag, opts)) < 0 || + (error = git_iterator_for_tree(&a, old_tree, &a_opts)) < 0 || + (error = git_iterator_for_tree(&b, new_tree, &b_opts)) < 0 || + (error = git_diff__from_iterators(&diff, repo, a, b, opts)) < 0) + goto out; + + *out = diff; + diff = NULL; +out: + git_iterator_free(a); + git_iterator_free(b); + git_diff_free(diff); + git__free(prefix); + + return error; +} + +static int diff_load_index(git_index **index, git_repository *repo) +{ + int error = git_repository_index__weakptr(index, repo); + + /* reload the repository index when user did not pass one in */ + if (!error && git_index_read(*index, false) < 0) + git_error_clear(); + + return error; +} + +int git_diff_tree_to_index( + git_diff **out, + git_repository *repo, + git_tree *old_tree, + git_index *index, + const git_diff_options *opts) +{ + git_iterator_flag_t iflag = GIT_ITERATOR_DONT_IGNORE_CASE | + GIT_ITERATOR_INCLUDE_CONFLICTS; + git_iterator_options a_opts = GIT_ITERATOR_OPTIONS_INIT, + b_opts = GIT_ITERATOR_OPTIONS_INIT; + git_iterator *a = NULL, *b = NULL; + git_diff *diff = NULL; + char *prefix = NULL; + bool index_ignore_case = false; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + *out = NULL; + + if (!index && (error = diff_load_index(&index, repo)) < 0) + return error; + + index_ignore_case = index->ignore_case; + + if ((error = diff_prepare_iterator_opts(&prefix, &a_opts, iflag, &b_opts, iflag, opts)) < 0 || + (error = git_iterator_for_tree(&a, old_tree, &a_opts)) < 0 || + (error = git_iterator_for_index(&b, repo, index, &b_opts)) < 0 || + (error = git_diff__from_iterators(&diff, repo, a, b, opts)) < 0) + goto out; + + /* if index is in case-insensitive order, re-sort deltas to match */ + if (index_ignore_case) + diff_set_ignore_case(diff, true); + + *out = diff; + diff = NULL; +out: + git_iterator_free(a); + git_iterator_free(b); + git_diff_free(diff); + git__free(prefix); + + return error; +} + +int git_diff_index_to_workdir( + git_diff **out, + git_repository *repo, + git_index *index, + const git_diff_options *opts) +{ + git_iterator_options a_opts = GIT_ITERATOR_OPTIONS_INIT, + b_opts = GIT_ITERATOR_OPTIONS_INIT; + git_iterator *a = NULL, *b = NULL; + git_diff *diff = NULL; + char *prefix = NULL; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + *out = NULL; + + if (!index && (error = diff_load_index(&index, repo)) < 0) + return error; + + if ((error = diff_prepare_iterator_opts(&prefix, &a_opts, GIT_ITERATOR_INCLUDE_CONFLICTS, + &b_opts, GIT_ITERATOR_DONT_AUTOEXPAND, opts)) < 0 || + (error = git_iterator_for_index(&a, repo, index, &a_opts)) < 0 || + (error = git_iterator_for_workdir(&b, repo, index, NULL, &b_opts)) < 0 || + (error = git_diff__from_iterators(&diff, repo, a, b, opts)) < 0) + goto out; + + if ((diff->opts.flags & GIT_DIFF_UPDATE_INDEX) && ((git_diff_generated *)diff)->index_updated) + if ((error = git_index_write(index)) < 0) + goto out; + + *out = diff; + diff = NULL; +out: + git_iterator_free(a); + git_iterator_free(b); + git_diff_free(diff); + git__free(prefix); + + return error; +} + +int git_diff_tree_to_workdir( + git_diff **out, + git_repository *repo, + git_tree *old_tree, + const git_diff_options *opts) +{ + git_iterator_options a_opts = GIT_ITERATOR_OPTIONS_INIT, + b_opts = GIT_ITERATOR_OPTIONS_INIT; + git_iterator *a = NULL, *b = NULL; + git_diff *diff = NULL; + char *prefix = NULL; + git_index *index; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + *out = NULL; + + if ((error = diff_prepare_iterator_opts(&prefix, &a_opts, 0, + &b_opts, GIT_ITERATOR_DONT_AUTOEXPAND, opts) < 0) || + (error = git_repository_index__weakptr(&index, repo)) < 0 || + (error = git_iterator_for_tree(&a, old_tree, &a_opts)) < 0 || + (error = git_iterator_for_workdir(&b, repo, index, old_tree, &b_opts)) < 0 || + (error = git_diff__from_iterators(&diff, repo, a, b, opts)) < 0) + goto out; + + *out = diff; + diff = NULL; +out: + git_iterator_free(a); + git_iterator_free(b); + git_diff_free(diff); + git__free(prefix); + + return error; +} + +int git_diff_tree_to_workdir_with_index( + git_diff **out, + git_repository *repo, + git_tree *tree, + const git_diff_options *opts) +{ + git_diff *d1 = NULL, *d2 = NULL; + git_index *index = NULL; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + *out = NULL; + + if ((error = diff_load_index(&index, repo)) < 0) + return error; + + if (!(error = git_diff_tree_to_index(&d1, repo, tree, index, opts)) && + !(error = git_diff_index_to_workdir(&d2, repo, index, opts))) + error = git_diff_merge(d1, d2); + + git_diff_free(d2); + + if (error) { + git_diff_free(d1); + d1 = NULL; + } + + *out = d1; + return error; +} + +int git_diff_index_to_index( + git_diff **out, + git_repository *repo, + git_index *old_index, + git_index *new_index, + const git_diff_options *opts) +{ + git_iterator_options a_opts = GIT_ITERATOR_OPTIONS_INIT, + b_opts = GIT_ITERATOR_OPTIONS_INIT; + git_iterator *a = NULL, *b = NULL; + git_diff *diff = NULL; + char *prefix = NULL; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(old_index); + GIT_ASSERT_ARG(new_index); + + *out = NULL; + + if ((error = diff_prepare_iterator_opts(&prefix, &a_opts, GIT_ITERATOR_DONT_IGNORE_CASE, + &b_opts, GIT_ITERATOR_DONT_IGNORE_CASE, opts) < 0) || + (error = git_iterator_for_index(&a, repo, old_index, &a_opts)) < 0 || + (error = git_iterator_for_index(&b, repo, new_index, &b_opts)) < 0 || + (error = git_diff__from_iterators(&diff, repo, a, b, opts)) < 0) + goto out; + + /* if index is in case-insensitive order, re-sort deltas to match */ + if (old_index->ignore_case || new_index->ignore_case) + diff_set_ignore_case(diff, true); + + *out = diff; + diff = NULL; +out: + git_iterator_free(a); + git_iterator_free(b); + git_diff_free(diff); + git__free(prefix); + + return error; +} + +int git_diff__paired_foreach( + git_diff *head2idx, + git_diff *idx2wd, + int (*cb)(git_diff_delta *h2i, git_diff_delta *i2w, void *payload), + void *payload) +{ + int cmp, error = 0; + git_diff_delta *h2i, *i2w; + size_t i, j, i_max, j_max; + int (*strcomp)(const char *, const char *) = git__strcmp; + bool h2i_icase, i2w_icase, icase_mismatch; + + i_max = head2idx ? head2idx->deltas.length : 0; + j_max = idx2wd ? idx2wd->deltas.length : 0; + if (!i_max && !j_max) + return 0; + + /* At some point, tree-to-index diffs will probably never ignore case, + * even if that isn't true now. Index-to-workdir diffs may or may not + * ignore case, but the index filename for the idx2wd diff should + * still be using the canonical case-preserving name. + * + * Therefore the main thing we need to do here is make sure the diffs + * are traversed in a compatible order. To do this, we temporarily + * resort a mismatched diff to get the order correct. + * + * In order to traverse renames in the index->workdir, we need to + * ensure that we compare the index name on both sides, so we + * always sort by the old name in the i2w list. + */ + h2i_icase = head2idx != NULL && git_diff_is_sorted_icase(head2idx); + i2w_icase = idx2wd != NULL && git_diff_is_sorted_icase(idx2wd); + + icase_mismatch = + (head2idx != NULL && idx2wd != NULL && h2i_icase != i2w_icase); + + if (icase_mismatch && h2i_icase) { + git_vector_set_cmp(&head2idx->deltas, git_diff_delta__cmp); + git_vector_sort(&head2idx->deltas); + } + + if (i2w_icase && !icase_mismatch) { + strcomp = git__strcasecmp; + + git_vector_set_cmp(&idx2wd->deltas, diff_delta_i2w_casecmp); + git_vector_sort(&idx2wd->deltas); + } else if (idx2wd != NULL) { + git_vector_set_cmp(&idx2wd->deltas, diff_delta_i2w_cmp); + git_vector_sort(&idx2wd->deltas); + } + + for (i = 0, j = 0; i < i_max || j < j_max; ) { + h2i = head2idx ? GIT_VECTOR_GET(&head2idx->deltas, i) : NULL; + i2w = idx2wd ? GIT_VECTOR_GET(&idx2wd->deltas, j) : NULL; + + cmp = !i2w ? -1 : !h2i ? 1 : + strcomp(h2i->new_file.path, i2w->old_file.path); + + if (cmp < 0) { + i++; i2w = NULL; + } else if (cmp > 0) { + j++; h2i = NULL; + } else { + i++; j++; + } + + if ((error = cb(h2i, i2w, payload)) != 0) { + git_error_set_after_callback(error); + break; + } + } + + /* restore case-insensitive delta sort */ + if (icase_mismatch && h2i_icase) { + git_vector_set_cmp(&head2idx->deltas, git_diff_delta__casecmp); + git_vector_sort(&head2idx->deltas); + } + + /* restore idx2wd sort by new path */ + if (idx2wd != NULL) { + git_vector_set_cmp(&idx2wd->deltas, + i2w_icase ? git_diff_delta__casecmp : git_diff_delta__cmp); + git_vector_sort(&idx2wd->deltas); + } + + return error; +} + +int git_diff__commit( + git_diff **out, + git_repository *repo, + const git_commit *commit, + const git_diff_options *opts) +{ + git_commit *parent = NULL; + git_diff *commit_diff = NULL; + git_tree *old_tree = NULL, *new_tree = NULL; + size_t parents; + int error = 0; + + *out = NULL; + + if ((parents = git_commit_parentcount(commit)) > 1) { + char commit_oidstr[GIT_OID_MAX_HEXSIZE + 1]; + + error = -1; + git_error_set(GIT_ERROR_INVALID, "commit %s is a merge commit", + git_oid_tostr(commit_oidstr, GIT_OID_MAX_HEXSIZE + 1, git_commit_id(commit))); + goto on_error; + } + + if (parents > 0) + if ((error = git_commit_parent(&parent, commit, 0)) < 0 || + (error = git_commit_tree(&old_tree, parent)) < 0) + goto on_error; + + if ((error = git_commit_tree(&new_tree, commit)) < 0 || + (error = git_diff_tree_to_tree(&commit_diff, repo, old_tree, new_tree, opts)) < 0) + goto on_error; + + *out = commit_diff; + +on_error: + git_tree_free(new_tree); + git_tree_free(old_tree); + git_commit_free(parent); + + return error; +} + diff --git a/src/libgit2/diff_generate.h b/src/libgit2/diff_generate.h new file mode 100644 index 0000000..b782f29 --- /dev/null +++ b/src/libgit2/diff_generate.h @@ -0,0 +1,130 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_diff_generate_h__ +#define INCLUDE_diff_generate_h__ + +#include "common.h" + +#include "diff.h" +#include "pool.h" +#include "index.h" + +enum { + GIT_DIFFCAPS_HAS_SYMLINKS = (1 << 0), /* symlinks on platform? */ + GIT_DIFFCAPS_IGNORE_STAT = (1 << 1), /* use stat? */ + GIT_DIFFCAPS_TRUST_MODE_BITS = (1 << 2), /* use st_mode? */ + GIT_DIFFCAPS_TRUST_CTIME = (1 << 3), /* use st_ctime? */ + GIT_DIFFCAPS_USE_DEV = (1 << 4) /* use st_dev? */ +}; + +#define DIFF_FLAGS_KNOWN_BINARY (GIT_DIFF_FLAG_BINARY|GIT_DIFF_FLAG_NOT_BINARY) +#define DIFF_FLAGS_NOT_BINARY (GIT_DIFF_FLAG_NOT_BINARY|GIT_DIFF_FLAG__NO_DATA) + +enum { + GIT_DIFF_FLAG__FREE_PATH = (1 << 7), /* `path` is allocated memory */ + GIT_DIFF_FLAG__FREE_DATA = (1 << 8), /* internal file data is allocated */ + GIT_DIFF_FLAG__UNMAP_DATA = (1 << 9), /* internal file data is mmap'ed */ + GIT_DIFF_FLAG__NO_DATA = (1 << 10), /* file data should not be loaded */ + GIT_DIFF_FLAG__FREE_BLOB = (1 << 11), /* release the blob when done */ + GIT_DIFF_FLAG__LOADED = (1 << 12), /* file data has been loaded */ + + GIT_DIFF_FLAG__TO_DELETE = (1 << 16), /* delete entry during rename det. */ + GIT_DIFF_FLAG__TO_SPLIT = (1 << 17), /* split entry during rename det. */ + GIT_DIFF_FLAG__IS_RENAME_TARGET = (1 << 18), + GIT_DIFF_FLAG__IS_RENAME_SOURCE = (1 << 19), + GIT_DIFF_FLAG__HAS_SELF_SIMILARITY = (1 << 20) +}; + +#define GIT_DIFF_FLAG__CLEAR_INTERNAL(F) (F) = ((F) & 0x00FFFF) + +#define GIT_DIFF__VERBOSE (1 << 30) + +extern void git_diff_addref(git_diff *diff); + +extern bool git_diff_delta__should_skip( + const git_diff_options *opts, const git_diff_delta *delta); + +extern int git_diff__from_iterators( + git_diff **diff_ptr, + git_repository *repo, + git_iterator *old_iter, + git_iterator *new_iter, + const git_diff_options *opts); + +extern int git_diff__commit( + git_diff **diff, git_repository *repo, const git_commit *commit, const git_diff_options *opts); + +extern int git_diff__paired_foreach( + git_diff *idx2head, + git_diff *wd2idx, + int (*cb)(git_diff_delta *i2h, git_diff_delta *w2i, void *payload), + void *payload); + +/* Merge two `git_diff`s according to the callback given by `cb`. */ + +typedef git_diff_delta *(*git_diff__merge_cb)( + const git_diff_delta *left, + const git_diff_delta *right, + git_pool *pool); + +extern int git_diff__merge( + git_diff *onto, const git_diff *from, git_diff__merge_cb cb); + +extern git_diff_delta *git_diff__merge_like_cgit( + const git_diff_delta *a, + const git_diff_delta *b, + git_pool *pool); + +/* Duplicate a `git_diff_delta` out of the `git_pool` */ +extern git_diff_delta *git_diff__delta_dup( + const git_diff_delta *d, git_pool *pool); + +extern int git_diff__oid_for_file( + git_oid *out, + git_diff *diff, + const char *path, + uint16_t mode, + git_object_size_t size); + +extern int git_diff__oid_for_entry( + git_oid *out, + git_diff *diff, + const git_index_entry *src, + uint16_t mode, + const git_oid *update_match); + +/* + * Sometimes a git_diff_file will have a zero size; this attempts to + * fill in the size without loading the blob if possible. If that is + * not possible, then it will return the git_odb_object that had to be + * loaded and the caller can use it or dispose of it as needed. + */ +GIT_INLINE(int) git_diff_file__resolve_zero_size( + git_diff_file *file, git_odb_object **odb_obj, git_repository *repo) +{ + int error; + git_odb *odb; + size_t len; + git_object_t type; + + if ((error = git_repository_odb(&odb, repo)) < 0) + return error; + + error = git_odb__read_header_or_object( + odb_obj, &len, &type, odb, &file->id); + + git_odb_free(odb); + + if (!error) { + file->size = (git_object_size_t)len; + file->flags |= GIT_DIFF_FLAG_VALID_SIZE; + } + + return error; +} + +#endif diff --git a/src/libgit2/diff_parse.c b/src/libgit2/diff_parse.c new file mode 100644 index 0000000..0460396 --- /dev/null +++ b/src/libgit2/diff_parse.c @@ -0,0 +1,123 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "diff_parse.h" + +#include "diff.h" +#include "patch.h" +#include "patch_parse.h" + +static void diff_parsed_free(git_diff *d) +{ + git_diff_parsed *diff = (git_diff_parsed *)d; + git_patch *patch; + size_t i; + + git_vector_foreach(&diff->patches, i, patch) + git_patch_free(patch); + + git_vector_free(&diff->patches); + + git_vector_free(&diff->base.deltas); + git_pool_clear(&diff->base.pool); + + git__memzero(diff, sizeof(*diff)); + git__free(diff); +} + +static git_diff_parsed *diff_parsed_alloc(git_oid_t oid_type) +{ + git_diff_parsed *diff; + + if ((diff = git__calloc(1, sizeof(git_diff_parsed))) == NULL) + return NULL; + + GIT_REFCOUNT_INC(&diff->base); + diff->base.type = GIT_DIFF_TYPE_PARSED; + diff->base.strcomp = git__strcmp; + diff->base.strncomp = git__strncmp; + diff->base.pfxcomp = git__prefixcmp; + diff->base.entrycomp = git_diff__entry_cmp; + diff->base.patch_fn = git_patch_parsed_from_diff; + diff->base.free_fn = diff_parsed_free; + + if (git_diff_options_init(&diff->base.opts, GIT_DIFF_OPTIONS_VERSION) < 0) { + git__free(diff); + return NULL; + } + + diff->base.opts.flags &= ~GIT_DIFF_IGNORE_CASE; + diff->base.opts.oid_type = oid_type; + + if (git_pool_init(&diff->base.pool, 1) < 0 || + git_vector_init(&diff->patches, 0, NULL) < 0 || + git_vector_init(&diff->base.deltas, 0, git_diff_delta__cmp) < 0) { + git_diff_free(&diff->base); + return NULL; + } + + git_vector_set_cmp(&diff->base.deltas, git_diff_delta__cmp); + + return diff; +} + +int git_diff_from_buffer( + git_diff **out, + const char *content, + size_t content_len +#ifdef GIT_EXPERIMENTAL_SHA256 + , git_diff_parse_options *opts +#endif + ) +{ + git_diff_parsed *diff; + git_patch *patch; + git_patch_parse_ctx *ctx = NULL; + git_patch_options patch_opts = GIT_PATCH_OPTIONS_INIT; + git_oid_t oid_type; + int error = 0; + + *out = NULL; + +#ifdef GIT_EXPERIMENTAL_SHA256 + oid_type = (opts && opts->oid_type) ? opts->oid_type : + GIT_OID_DEFAULT; +#else + oid_type = GIT_OID_DEFAULT; +#endif + + patch_opts.oid_type = oid_type; + + diff = diff_parsed_alloc(oid_type); + GIT_ERROR_CHECK_ALLOC(diff); + + ctx = git_patch_parse_ctx_init(content, content_len, &patch_opts); + GIT_ERROR_CHECK_ALLOC(ctx); + + while (ctx->parse_ctx.remain_len) { + if ((error = git_patch_parse(&patch, ctx)) < 0) + break; + + git_vector_insert(&diff->patches, patch); + git_vector_insert(&diff->base.deltas, patch->delta); + } + + if (error == GIT_ENOTFOUND && git_vector_length(&diff->patches) > 0) { + git_error_clear(); + error = 0; + } + + git_patch_parse_ctx_free(ctx); + + if (error < 0) + git_diff_free(&diff->base); + else + *out = &diff->base; + + return error; +} + diff --git a/src/libgit2/diff_parse.h b/src/libgit2/diff_parse.h new file mode 100644 index 0000000..8767821 --- /dev/null +++ b/src/libgit2/diff_parse.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_diff_parse_h__ +#define INCLUDE_diff_parse_h__ + +#include "common.h" + +#include "diff.h" + +typedef struct { + struct git_diff base; + + git_vector patches; +} git_diff_parsed; + +#endif diff --git a/src/libgit2/diff_print.c b/src/libgit2/diff_print.c new file mode 100644 index 0000000..32c9368 --- /dev/null +++ b/src/libgit2/diff_print.c @@ -0,0 +1,849 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "buf.h" +#include "diff.h" +#include "diff_file.h" +#include "patch_generate.h" +#include "futils.h" +#include "zstream.h" +#include "blob.h" +#include "delta.h" +#include "git2/sys/diff.h" + +typedef struct { + git_diff_format_t format; + git_diff_line_cb print_cb; + void *payload; + + git_str *buf; + git_diff_line line; + + const char *old_prefix; + const char *new_prefix; + uint32_t flags; + int id_strlen; + git_oid_t oid_type; + + int (*strcomp)(const char *, const char *); +} diff_print_info; + +static int diff_print_info_init__common( + diff_print_info *pi, + git_str *out, + git_repository *repo, + git_diff_format_t format, + git_diff_line_cb cb, + void *payload) +{ + pi->format = format; + pi->print_cb = cb; + pi->payload = payload; + pi->buf = out; + + GIT_ASSERT(pi->oid_type); + + if (!pi->id_strlen) { + if (!repo) + pi->id_strlen = GIT_ABBREV_DEFAULT; + else if (git_repository__configmap_lookup(&pi->id_strlen, repo, GIT_CONFIGMAP_ABBREV) < 0) + return -1; + } + + if (pi->id_strlen > 0 && + (size_t)pi->id_strlen > git_oid_hexsize(pi->oid_type)) + pi->id_strlen = (int)git_oid_hexsize(pi->oid_type); + + memset(&pi->line, 0, sizeof(pi->line)); + pi->line.old_lineno = -1; + pi->line.new_lineno = -1; + pi->line.num_lines = 1; + + return 0; +} + +static int diff_print_info_init_fromdiff( + diff_print_info *pi, + git_str *out, + git_diff *diff, + git_diff_format_t format, + git_diff_line_cb cb, + void *payload) +{ + git_repository *repo = diff ? diff->repo : NULL; + + memset(pi, 0, sizeof(diff_print_info)); + + if (diff) { + pi->flags = diff->opts.flags; + pi->oid_type = diff->opts.oid_type; + pi->id_strlen = diff->opts.id_abbrev; + pi->old_prefix = diff->opts.old_prefix; + pi->new_prefix = diff->opts.new_prefix; + + pi->strcomp = diff->strcomp; + } + + return diff_print_info_init__common(pi, out, repo, format, cb, payload); +} + +static int diff_print_info_init_frompatch( + diff_print_info *pi, + git_str *out, + git_patch *patch, + git_diff_format_t format, + git_diff_line_cb cb, + void *payload) +{ + GIT_ASSERT_ARG(patch); + + memset(pi, 0, sizeof(diff_print_info)); + + pi->flags = patch->diff_opts.flags; + pi->oid_type = patch->diff_opts.oid_type; + pi->id_strlen = patch->diff_opts.id_abbrev; + pi->old_prefix = patch->diff_opts.old_prefix; + pi->new_prefix = patch->diff_opts.new_prefix; + + return diff_print_info_init__common(pi, out, patch->repo, format, cb, payload); +} + +static char diff_pick_suffix(int mode) +{ + if (S_ISDIR(mode)) + return '/'; + else if (GIT_PERMS_IS_EXEC(mode)) /* -V536 */ + /* in git, modes are very regular, so we must have 0100755 mode */ + return '*'; + else + return ' '; +} + +char git_diff_status_char(git_delta_t status) +{ + char code; + + switch (status) { + case GIT_DELTA_ADDED: code = 'A'; break; + case GIT_DELTA_DELETED: code = 'D'; break; + case GIT_DELTA_MODIFIED: code = 'M'; break; + case GIT_DELTA_RENAMED: code = 'R'; break; + case GIT_DELTA_COPIED: code = 'C'; break; + case GIT_DELTA_IGNORED: code = 'I'; break; + case GIT_DELTA_UNTRACKED: code = '?'; break; + case GIT_DELTA_TYPECHANGE: code = 'T'; break; + case GIT_DELTA_UNREADABLE: code = 'X'; break; + default: code = ' '; break; + } + + return code; +} + +static int diff_print_one_name_only( + const git_diff_delta *delta, float progress, void *data) +{ + diff_print_info *pi = data; + git_str *out = pi->buf; + + GIT_UNUSED(progress); + + if ((pi->flags & GIT_DIFF_SHOW_UNMODIFIED) == 0 && + delta->status == GIT_DELTA_UNMODIFIED) + return 0; + + git_str_clear(out); + git_str_puts(out, delta->new_file.path); + git_str_putc(out, '\n'); + if (git_str_oom(out)) + return -1; + + pi->line.origin = GIT_DIFF_LINE_FILE_HDR; + pi->line.content = git_str_cstr(out); + pi->line.content_len = git_str_len(out); + + return pi->print_cb(delta, NULL, &pi->line, pi->payload); +} + +static int diff_print_one_name_status( + const git_diff_delta *delta, float progress, void *data) +{ + diff_print_info *pi = data; + git_str *out = pi->buf; + char old_suffix, new_suffix, code = git_diff_status_char(delta->status); + int(*strcomp)(const char *, const char *) = pi->strcomp ? + pi->strcomp : git__strcmp; + + GIT_UNUSED(progress); + + if ((pi->flags & GIT_DIFF_SHOW_UNMODIFIED) == 0 && code == ' ') + return 0; + + old_suffix = diff_pick_suffix(delta->old_file.mode); + new_suffix = diff_pick_suffix(delta->new_file.mode); + + git_str_clear(out); + + if (delta->old_file.path != delta->new_file.path && + strcomp(delta->old_file.path,delta->new_file.path) != 0) + git_str_printf(out, "%c\t%s%c %s%c\n", code, + delta->old_file.path, old_suffix, delta->new_file.path, new_suffix); + else if (delta->old_file.mode != delta->new_file.mode && + delta->old_file.mode != 0 && delta->new_file.mode != 0) + git_str_printf(out, "%c\t%s%c %s%c\n", code, + delta->old_file.path, old_suffix, delta->new_file.path, new_suffix); + else if (old_suffix != ' ') + git_str_printf(out, "%c\t%s%c\n", code, delta->old_file.path, old_suffix); + else + git_str_printf(out, "%c\t%s\n", code, delta->old_file.path); + if (git_str_oom(out)) + return -1; + + pi->line.origin = GIT_DIFF_LINE_FILE_HDR; + pi->line.content = git_str_cstr(out); + pi->line.content_len = git_str_len(out); + + return pi->print_cb(delta, NULL, &pi->line, pi->payload); +} + +static int diff_print_one_raw( + const git_diff_delta *delta, float progress, void *data) +{ + diff_print_info *pi = data; + git_str *out = pi->buf; + int id_abbrev; + char code = git_diff_status_char(delta->status); + char start_oid[GIT_OID_MAX_HEXSIZE + 1], + end_oid[GIT_OID_MAX_HEXSIZE + 1]; + size_t oid_hexsize; + bool id_is_abbrev; + + GIT_UNUSED(progress); + + if ((pi->flags & GIT_DIFF_SHOW_UNMODIFIED) == 0 && code == ' ') + return 0; + + git_str_clear(out); + + id_abbrev = delta->old_file.mode ? delta->old_file.id_abbrev : + delta->new_file.id_abbrev; + + if (pi->id_strlen > id_abbrev) { + git_error_set(GIT_ERROR_PATCH, + "the patch input contains %d id characters (cannot print %d)", + id_abbrev, pi->id_strlen); + return -1; + } + +#ifdef GIT_EXPERIMENTAL_SHA256 + GIT_ASSERT(delta->old_file.id.type == delta->new_file.id.type); + oid_hexsize = git_oid_hexsize(delta->old_file.id.type); +#else + oid_hexsize = GIT_OID_SHA1_HEXSIZE; +#endif + + id_is_abbrev = (pi->id_strlen > 0 && + (size_t)pi->id_strlen <= oid_hexsize); + + git_oid_tostr(start_oid, pi->id_strlen + 1, &delta->old_file.id); + git_oid_tostr(end_oid, pi->id_strlen + 1, &delta->new_file.id); + + git_str_printf(out, + id_is_abbrev ? ":%06o %06o %s... %s... %c" : ":%06o %06o %s %s %c", + delta->old_file.mode, delta->new_file.mode, start_oid, end_oid, code); + + if (delta->similarity > 0) + git_str_printf(out, "%03u", delta->similarity); + + if (delta->old_file.path != delta->new_file.path) + git_str_printf( + out, "\t%s %s\n", delta->old_file.path, delta->new_file.path); + else + git_str_printf( + out, "\t%s\n", delta->old_file.path ? + delta->old_file.path : delta->new_file.path); + + if (git_str_oom(out)) + return -1; + + pi->line.origin = GIT_DIFF_LINE_FILE_HDR; + pi->line.content = git_str_cstr(out); + pi->line.content_len = git_str_len(out); + + return pi->print_cb(delta, NULL, &pi->line, pi->payload); +} + +static int diff_print_modes( + git_str *out, const git_diff_delta *delta) +{ + git_str_printf(out, "old mode %o\n", delta->old_file.mode); + git_str_printf(out, "new mode %o\n", delta->new_file.mode); + + return git_str_oom(out) ? -1 : 0; +} + +static int diff_print_oid_range( + git_str *out, const git_diff_delta *delta, int id_strlen, + bool print_index) +{ + char start_oid[GIT_OID_MAX_HEXSIZE + 1], + end_oid[GIT_OID_MAX_HEXSIZE + 1]; + + if (delta->old_file.mode && + id_strlen > delta->old_file.id_abbrev) { + git_error_set(GIT_ERROR_PATCH, + "the patch input contains %d id characters (cannot print %d)", + delta->old_file.id_abbrev, id_strlen); + return -1; + } + + if ((delta->new_file.mode && + id_strlen > delta->new_file.id_abbrev)) { + git_error_set(GIT_ERROR_PATCH, + "the patch input contains %d id characters (cannot print %d)", + delta->new_file.id_abbrev, id_strlen); + return -1; + } + + git_oid_tostr(start_oid, id_strlen + 1, &delta->old_file.id); + git_oid_tostr(end_oid, id_strlen + 1, &delta->new_file.id); + + if (delta->old_file.mode == delta->new_file.mode) { + if (print_index) + git_str_printf(out, "index %s..%s %o\n", + start_oid, end_oid, delta->old_file.mode); + } else { + if (delta->old_file.mode == 0) + git_str_printf(out, "new file mode %o\n", delta->new_file.mode); + else if (delta->new_file.mode == 0) + git_str_printf(out, "deleted file mode %o\n", delta->old_file.mode); + else + diff_print_modes(out, delta); + + if (print_index) + git_str_printf(out, "index %s..%s\n", start_oid, end_oid); + } + + return git_str_oom(out) ? -1 : 0; +} + +static int diff_delta_format_path( + git_str *out, const char *prefix, const char *filename) +{ + if (!filename) { + /* don't prefix "/dev/null" */ + return git_str_puts(out, "/dev/null"); + } + + if (git_str_joinpath(out, prefix, filename) < 0) + return -1; + + return git_str_quote(out); +} + +static int diff_delta_format_with_paths( + git_str *out, + const git_diff_delta *delta, + const char *template, + const char *oldpath, + const char *newpath) +{ + if (git_oid_is_zero(&delta->old_file.id)) + oldpath = "/dev/null"; + + if (git_oid_is_zero(&delta->new_file.id)) + newpath = "/dev/null"; + + return git_str_printf(out, template, oldpath, newpath); +} + +static int diff_delta_format_similarity_header( + git_str *out, + const git_diff_delta *delta) +{ + git_str old_path = GIT_STR_INIT, new_path = GIT_STR_INIT; + const char *type; + int error = 0; + + if (delta->similarity > 100) { + git_error_set(GIT_ERROR_PATCH, "invalid similarity %d", delta->similarity); + error = -1; + goto done; + } + + GIT_ASSERT(delta->status == GIT_DELTA_RENAMED || delta->status == GIT_DELTA_COPIED); + if (delta->status == GIT_DELTA_RENAMED) + type = "rename"; + else + type = "copy"; + + if ((error = git_str_puts(&old_path, delta->old_file.path)) < 0 || + (error = git_str_puts(&new_path, delta->new_file.path)) < 0 || + (error = git_str_quote(&old_path)) < 0 || + (error = git_str_quote(&new_path)) < 0) + goto done; + + git_str_printf(out, + "similarity index %d%%\n" + "%s from %s\n" + "%s to %s\n", + delta->similarity, + type, old_path.ptr, + type, new_path.ptr); + + if (git_str_oom(out)) + error = -1; + +done: + git_str_dispose(&old_path); + git_str_dispose(&new_path); + + return error; +} + +static bool delta_is_unchanged(const git_diff_delta *delta) +{ + if (git_oid_is_zero(&delta->old_file.id) && + git_oid_is_zero(&delta->new_file.id)) + return true; + + if (delta->old_file.mode == GIT_FILEMODE_COMMIT || + delta->new_file.mode == GIT_FILEMODE_COMMIT) + return false; + + if (git_oid_equal(&delta->old_file.id, &delta->new_file.id)) + return true; + + return false; +} + +int git_diff_delta__format_file_header( + git_str *out, + const git_diff_delta *delta, + const char *oldpfx, + const char *newpfx, + int id_strlen, + bool print_index) +{ + git_str old_path = GIT_STR_INIT, new_path = GIT_STR_INIT; + bool unchanged = delta_is_unchanged(delta); + int error = 0; + + if (!oldpfx) + oldpfx = DIFF_OLD_PREFIX_DEFAULT; + if (!newpfx) + newpfx = DIFF_NEW_PREFIX_DEFAULT; + if (!id_strlen) + id_strlen = GIT_ABBREV_DEFAULT; + + if ((error = diff_delta_format_path( + &old_path, oldpfx, delta->old_file.path)) < 0 || + (error = diff_delta_format_path( + &new_path, newpfx, delta->new_file.path)) < 0) + goto done; + + git_str_clear(out); + + git_str_printf(out, "diff --git %s %s\n", + old_path.ptr, new_path.ptr); + + if (unchanged && delta->old_file.mode != delta->new_file.mode) + diff_print_modes(out, delta); + + if (delta->status == GIT_DELTA_RENAMED || + (delta->status == GIT_DELTA_COPIED && unchanged)) { + if ((error = diff_delta_format_similarity_header(out, delta)) < 0) + goto done; + } + + if (!unchanged) { + if ((error = diff_print_oid_range(out, delta, + id_strlen, print_index)) < 0) + goto done; + + if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) + diff_delta_format_with_paths(out, delta, + "--- %s\n+++ %s\n", old_path.ptr, new_path.ptr); + } + + if (git_str_oom(out)) + error = -1; + +done: + git_str_dispose(&old_path); + git_str_dispose(&new_path); + + return error; +} + +static int format_binary( + diff_print_info *pi, + git_diff_binary_t type, + const char *data, + size_t datalen, + size_t inflatedlen) +{ + const char *typename = type == GIT_DIFF_BINARY_DELTA ? + "delta" : "literal"; + const char *scan, *end; + + git_str_printf(pi->buf, "%s %" PRIuZ "\n", typename, inflatedlen); + pi->line.num_lines++; + + for (scan = data, end = data + datalen; scan < end; ) { + size_t chunk_len = end - scan; + if (chunk_len > 52) + chunk_len = 52; + + if (chunk_len <= 26) + git_str_putc(pi->buf, (char)chunk_len + 'A' - 1); + else + git_str_putc(pi->buf, (char)chunk_len - 26 + 'a' - 1); + + git_str_encode_base85(pi->buf, scan, chunk_len); + git_str_putc(pi->buf, '\n'); + + if (git_str_oom(pi->buf)) + return -1; + + scan += chunk_len; + pi->line.num_lines++; + } + git_str_putc(pi->buf, '\n'); + + if (git_str_oom(pi->buf)) + return -1; + + return 0; +} + +static int diff_print_patch_file_binary_noshow( + diff_print_info *pi, git_diff_delta *delta, + const char *old_pfx, const char *new_pfx) +{ + git_str old_path = GIT_STR_INIT, new_path = GIT_STR_INIT; + int error; + + if ((error = diff_delta_format_path(&old_path, old_pfx, delta->old_file.path)) < 0 || + (error = diff_delta_format_path(&new_path, new_pfx, delta->new_file.path)) < 0 || + (error = diff_delta_format_with_paths(pi->buf, delta, "Binary files %s and %s differ\n", + old_path.ptr, new_path.ptr)) < 0) + goto done; + + pi->line.num_lines = 1; + +done: + git_str_dispose(&old_path); + git_str_dispose(&new_path); + return error; +} + +static int diff_print_patch_file_binary( + diff_print_info *pi, git_diff_delta *delta, + const char *old_pfx, const char *new_pfx, + const git_diff_binary *binary) +{ + size_t pre_binary_size; + int error; + + if (delta->status == GIT_DELTA_UNMODIFIED) + return 0; + + if ((pi->flags & GIT_DIFF_SHOW_BINARY) == 0 || !binary->contains_data) + return diff_print_patch_file_binary_noshow( + pi, delta, old_pfx, new_pfx); + + pre_binary_size = pi->buf->size; + git_str_printf(pi->buf, "GIT binary patch\n"); + pi->line.num_lines++; + + if ((error = format_binary(pi, binary->new_file.type, binary->new_file.data, + binary->new_file.datalen, binary->new_file.inflatedlen)) < 0 || + (error = format_binary(pi, binary->old_file.type, binary->old_file.data, + binary->old_file.datalen, binary->old_file.inflatedlen)) < 0) { + if (error == GIT_EBUFS) { + git_error_clear(); + git_str_truncate(pi->buf, pre_binary_size); + + return diff_print_patch_file_binary_noshow( + pi, delta, old_pfx, new_pfx); + } + } + + pi->line.num_lines++; + return error; +} + +static int diff_print_patch_file( + const git_diff_delta *delta, float progress, void *data) +{ + int error; + diff_print_info *pi = data; + const char *oldpfx = + pi->old_prefix ? pi->old_prefix : DIFF_OLD_PREFIX_DEFAULT; + const char *newpfx = + pi->new_prefix ? pi->new_prefix : DIFF_NEW_PREFIX_DEFAULT; + + bool binary = (delta->flags & GIT_DIFF_FLAG_BINARY) || + (pi->flags & GIT_DIFF_FORCE_BINARY); + bool show_binary = !!(pi->flags & GIT_DIFF_SHOW_BINARY); + int id_strlen = pi->id_strlen; + bool print_index = (pi->format != GIT_DIFF_FORMAT_PATCH_ID); + + if (binary && show_binary) + id_strlen = delta->old_file.id_abbrev ? delta->old_file.id_abbrev : + delta->new_file.id_abbrev; + + GIT_UNUSED(progress); + + if (S_ISDIR(delta->new_file.mode) || + delta->status == GIT_DELTA_UNMODIFIED || + delta->status == GIT_DELTA_IGNORED || + delta->status == GIT_DELTA_UNREADABLE || + (delta->status == GIT_DELTA_UNTRACKED && + (pi->flags & GIT_DIFF_SHOW_UNTRACKED_CONTENT) == 0)) + return 0; + + if ((error = git_diff_delta__format_file_header(pi->buf, delta, oldpfx, newpfx, + id_strlen, print_index)) < 0) + return error; + + pi->line.origin = GIT_DIFF_LINE_FILE_HDR; + pi->line.content = git_str_cstr(pi->buf); + pi->line.content_len = git_str_len(pi->buf); + + return pi->print_cb(delta, NULL, &pi->line, pi->payload); +} + +static int diff_print_patch_binary( + const git_diff_delta *delta, + const git_diff_binary *binary, + void *data) +{ + diff_print_info *pi = data; + const char *old_pfx = + pi->old_prefix ? pi->old_prefix : DIFF_OLD_PREFIX_DEFAULT; + const char *new_pfx = + pi->new_prefix ? pi->new_prefix : DIFF_NEW_PREFIX_DEFAULT; + int error; + + git_str_clear(pi->buf); + + if ((error = diff_print_patch_file_binary( + pi, (git_diff_delta *)delta, old_pfx, new_pfx, binary)) < 0) + return error; + + pi->line.origin = GIT_DIFF_LINE_BINARY; + pi->line.content = git_str_cstr(pi->buf); + pi->line.content_len = git_str_len(pi->buf); + + return pi->print_cb(delta, NULL, &pi->line, pi->payload); +} + +static int diff_print_patch_hunk( + const git_diff_delta *d, + const git_diff_hunk *h, + void *data) +{ + diff_print_info *pi = data; + + if (S_ISDIR(d->new_file.mode)) + return 0; + + pi->line.origin = GIT_DIFF_LINE_HUNK_HDR; + pi->line.content = h->header; + pi->line.content_len = h->header_len; + + return pi->print_cb(d, h, &pi->line, pi->payload); +} + +static int diff_print_patch_line( + const git_diff_delta *delta, + const git_diff_hunk *hunk, + const git_diff_line *line, + void *data) +{ + diff_print_info *pi = data; + + if (S_ISDIR(delta->new_file.mode)) + return 0; + + return pi->print_cb(delta, hunk, line, pi->payload); +} + +/* print a git_diff to an output callback */ +int git_diff_print( + git_diff *diff, + git_diff_format_t format, + git_diff_line_cb print_cb, + void *payload) +{ + int error; + git_str buf = GIT_STR_INIT; + diff_print_info pi; + git_diff_file_cb print_file = NULL; + git_diff_binary_cb print_binary = NULL; + git_diff_hunk_cb print_hunk = NULL; + git_diff_line_cb print_line = NULL; + + switch (format) { + case GIT_DIFF_FORMAT_PATCH: + print_file = diff_print_patch_file; + print_binary = diff_print_patch_binary; + print_hunk = diff_print_patch_hunk; + print_line = diff_print_patch_line; + break; + case GIT_DIFF_FORMAT_PATCH_ID: + print_file = diff_print_patch_file; + print_binary = diff_print_patch_binary; + print_line = diff_print_patch_line; + break; + case GIT_DIFF_FORMAT_PATCH_HEADER: + print_file = diff_print_patch_file; + break; + case GIT_DIFF_FORMAT_RAW: + print_file = diff_print_one_raw; + break; + case GIT_DIFF_FORMAT_NAME_ONLY: + print_file = diff_print_one_name_only; + break; + case GIT_DIFF_FORMAT_NAME_STATUS: + print_file = diff_print_one_name_status; + break; + default: + git_error_set(GIT_ERROR_INVALID, "unknown diff output format (%d)", format); + return -1; + } + + if ((error = diff_print_info_init_fromdiff(&pi, &buf, diff, format, print_cb, payload)) < 0) + goto out; + + if ((error = git_diff_foreach(diff, print_file, print_binary, print_hunk, print_line, &pi)) != 0) { + git_error_set_after_callback_function(error, "git_diff_print"); + goto out; + } + +out: + git_str_dispose(&buf); + return error; +} + +int git_diff_print_callback__to_buf( + const git_diff_delta *delta, + const git_diff_hunk *hunk, + const git_diff_line *line, + void *payload) +{ + git_str *output = payload; + GIT_UNUSED(delta); GIT_UNUSED(hunk); + + if (!output) { + git_error_set(GIT_ERROR_INVALID, "buffer pointer must be provided"); + return -1; + } + + if (line->origin == GIT_DIFF_LINE_ADDITION || + line->origin == GIT_DIFF_LINE_DELETION || + line->origin == GIT_DIFF_LINE_CONTEXT) + git_str_putc(output, line->origin); + + return git_str_put(output, line->content, line->content_len); +} + +int git_diff_print_callback__to_file_handle( + const git_diff_delta *delta, + const git_diff_hunk *hunk, + const git_diff_line *line, + void *payload) +{ + FILE *fp = payload ? payload : stdout; + int error; + + GIT_UNUSED(delta); + GIT_UNUSED(hunk); + + if (line->origin == GIT_DIFF_LINE_CONTEXT || + line->origin == GIT_DIFF_LINE_ADDITION || + line->origin == GIT_DIFF_LINE_DELETION) { + while ((error = fputc(line->origin, fp)) == EINTR) + continue; + if (error) { + git_error_set(GIT_ERROR_OS, "could not write status"); + return -1; + } + } + + if (fwrite(line->content, line->content_len, 1, fp) != 1) { + git_error_set(GIT_ERROR_OS, "could not write line"); + return -1; + } + + return 0; +} + +/* print a git_diff to a git_str */ +int git_diff_to_buf(git_buf *out, git_diff *diff, git_diff_format_t format) +{ + git_str str = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(diff); + + if ((error = git_buf_tostr(&str, out)) < 0 || + (error = git_diff_print(diff, format, git_diff_print_callback__to_buf, &str)) < 0) + goto done; + + error = git_buf_fromstr(out, &str); + +done: + git_str_dispose(&str); + return error; +} + +/* print a git_patch to an output callback */ +int git_patch_print( + git_patch *patch, + git_diff_line_cb print_cb, + void *payload) +{ + git_str temp = GIT_STR_INIT; + diff_print_info pi; + int error; + + GIT_ASSERT_ARG(patch); + GIT_ASSERT_ARG(print_cb); + + if ((error = diff_print_info_init_frompatch(&pi, &temp, patch, + GIT_DIFF_FORMAT_PATCH, print_cb, payload)) < 0) + goto out; + + if ((error = git_patch__invoke_callbacks(patch, diff_print_patch_file, diff_print_patch_binary, + diff_print_patch_hunk, diff_print_patch_line, &pi)) < 0) { + git_error_set_after_callback_function(error, "git_patch_print"); + goto out; + } + +out: + git_str_dispose(&temp); + return error; +} + +/* print a git_patch to a git_str */ +int git_patch_to_buf(git_buf *out, git_patch *patch) +{ + GIT_BUF_WRAP_PRIVATE(out, git_patch__to_buf, patch); +} + +int git_patch__to_buf(git_str *out, git_patch *patch) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(patch); + + return git_patch_print(patch, git_diff_print_callback__to_buf, out); +} diff --git a/src/libgit2/diff_stats.c b/src/libgit2/diff_stats.c new file mode 100644 index 0000000..2599398 --- /dev/null +++ b/src/libgit2/diff_stats.c @@ -0,0 +1,376 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "diff_stats.h" + +#include "buf.h" +#include "common.h" +#include "vector.h" +#include "diff.h" +#include "patch_generate.h" + +#define DIFF_RENAME_FILE_SEPARATOR " => " +#define STATS_FULL_MIN_SCALE 7 + +typedef struct { + size_t insertions; + size_t deletions; +} diff_file_stats; + +struct git_diff_stats { + git_diff *diff; + diff_file_stats *filestats; + + size_t files_changed; + size_t insertions; + size_t deletions; + size_t renames; + + size_t max_name; + size_t max_filestat; + int max_digits; +}; + +static int digits_for_value(size_t val) +{ + int count = 1; + size_t placevalue = 10; + + while (val >= placevalue) { + ++count; + placevalue *= 10; + } + + return count; +} + +static int diff_file_stats_full_to_buf( + git_str *out, + const git_diff_delta *delta, + const diff_file_stats *filestat, + const git_diff_stats *stats, + size_t width) +{ + const char *old_path = NULL, *new_path = NULL, *adddel_path = NULL; + size_t padding; + git_object_size_t old_size, new_size; + + old_path = delta->old_file.path; + new_path = delta->new_file.path; + old_size = delta->old_file.size; + new_size = delta->new_file.size; + + if (old_path && new_path && strcmp(old_path, new_path) != 0) { + size_t common_dirlen; + int error; + + padding = stats->max_name - strlen(old_path) - strlen(new_path); + + if ((common_dirlen = git_fs_path_common_dirlen(old_path, new_path)) && + common_dirlen <= INT_MAX) { + error = git_str_printf(out, " %.*s{%s"DIFF_RENAME_FILE_SEPARATOR"%s}", + (int) common_dirlen, old_path, + old_path + common_dirlen, + new_path + common_dirlen); + } else { + error = git_str_printf(out, " %s" DIFF_RENAME_FILE_SEPARATOR "%s", + old_path, new_path); + } + + if (error < 0) + goto on_error; + } else { + adddel_path = new_path ? new_path : old_path; + if (git_str_printf(out, " %s", adddel_path) < 0) + goto on_error; + + padding = stats->max_name - strlen(adddel_path); + + if (stats->renames > 0) + padding += strlen(DIFF_RENAME_FILE_SEPARATOR); + } + + if (git_str_putcn(out, ' ', padding) < 0 || + git_str_puts(out, " | ") < 0) + goto on_error; + + if (delta->flags & GIT_DIFF_FLAG_BINARY) { + if (git_str_printf(out, + "Bin %" PRId64 " -> %" PRId64 " bytes", old_size, new_size) < 0) + goto on_error; + } + else { + if (git_str_printf(out, + "%*" PRIuZ, stats->max_digits, + filestat->insertions + filestat->deletions) < 0) + goto on_error; + + if (filestat->insertions || filestat->deletions) { + if (git_str_putc(out, ' ') < 0) + goto on_error; + + if (!width) { + if (git_str_putcn(out, '+', filestat->insertions) < 0 || + git_str_putcn(out, '-', filestat->deletions) < 0) + goto on_error; + } else { + size_t total = filestat->insertions + filestat->deletions; + size_t full = (total * width + stats->max_filestat / 2) / + stats->max_filestat; + size_t plus = full * filestat->insertions / total; + size_t minus = full - plus; + + if (git_str_putcn(out, '+', max(plus, 1)) < 0 || + git_str_putcn(out, '-', max(minus, 1)) < 0) + goto on_error; + } + } + } + + git_str_putc(out, '\n'); + +on_error: + return (git_str_oom(out) ? -1 : 0); +} + +static int diff_file_stats_number_to_buf( + git_str *out, + const git_diff_delta *delta, + const diff_file_stats *filestats) +{ + int error; + const char *path = delta->new_file.path; + + if (delta->flags & GIT_DIFF_FLAG_BINARY) + error = git_str_printf(out, "%-8c" "%-8c" "%s\n", '-', '-', path); + else + error = git_str_printf(out, "%-8" PRIuZ "%-8" PRIuZ "%s\n", + filestats->insertions, filestats->deletions, path); + + return error; +} + +static int diff_file_stats_summary_to_buf( + git_str *out, + const git_diff_delta *delta) +{ + if (delta->old_file.mode != delta->new_file.mode) { + if (delta->old_file.mode == 0) { + git_str_printf(out, " create mode %06o %s\n", + delta->new_file.mode, delta->new_file.path); + } + else if (delta->new_file.mode == 0) { + git_str_printf(out, " delete mode %06o %s\n", + delta->old_file.mode, delta->old_file.path); + } + else { + git_str_printf(out, " mode change %06o => %06o %s\n", + delta->old_file.mode, delta->new_file.mode, delta->new_file.path); + } + } + + return 0; +} + +int git_diff_get_stats( + git_diff_stats **out, + git_diff *diff) +{ + size_t i, deltas; + size_t total_insertions = 0, total_deletions = 0; + git_diff_stats *stats = NULL; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(diff); + + stats = git__calloc(1, sizeof(git_diff_stats)); + GIT_ERROR_CHECK_ALLOC(stats); + + deltas = git_diff_num_deltas(diff); + + stats->filestats = git__calloc(deltas, sizeof(diff_file_stats)); + if (!stats->filestats) { + git__free(stats); + return -1; + } + + stats->diff = diff; + GIT_REFCOUNT_INC(diff); + + for (i = 0; i < deltas && !error; ++i) { + git_patch *patch = NULL; + size_t add = 0, remove = 0, namelen; + const git_diff_delta *delta; + + if ((error = git_patch_from_diff(&patch, diff, i)) < 0) + break; + + /* keep a count of renames because it will affect formatting */ + delta = patch->delta; + + /* TODO ugh */ + namelen = strlen(delta->new_file.path); + if (delta->old_file.path && strcmp(delta->old_file.path, delta->new_file.path) != 0) { + namelen += strlen(delta->old_file.path); + stats->renames++; + } + + /* and, of course, count the line stats */ + error = git_patch_line_stats(NULL, &add, &remove, patch); + + git_patch_free(patch); + + stats->filestats[i].insertions = add; + stats->filestats[i].deletions = remove; + + total_insertions += add; + total_deletions += remove; + + if (stats->max_name < namelen) + stats->max_name = namelen; + if (stats->max_filestat < add + remove) + stats->max_filestat = add + remove; + } + + stats->files_changed = deltas; + stats->insertions = total_insertions; + stats->deletions = total_deletions; + stats->max_digits = digits_for_value(stats->max_filestat + 1); + + if (error < 0) { + git_diff_stats_free(stats); + stats = NULL; + } + + *out = stats; + return error; +} + +size_t git_diff_stats_files_changed( + const git_diff_stats *stats) +{ + GIT_ASSERT_ARG(stats); + + return stats->files_changed; +} + +size_t git_diff_stats_insertions( + const git_diff_stats *stats) +{ + GIT_ASSERT_ARG(stats); + + return stats->insertions; +} + +size_t git_diff_stats_deletions( + const git_diff_stats *stats) +{ + GIT_ASSERT_ARG(stats); + + return stats->deletions; +} + +int git_diff_stats_to_buf( + git_buf *out, + const git_diff_stats *stats, + git_diff_stats_format_t format, + size_t width) +{ + GIT_BUF_WRAP_PRIVATE(out, git_diff__stats_to_buf, stats, format, width); +} + +int git_diff__stats_to_buf( + git_str *out, + const git_diff_stats *stats, + git_diff_stats_format_t format, + size_t width) +{ + int error = 0; + size_t i; + const git_diff_delta *delta; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(stats); + + if (format & GIT_DIFF_STATS_NUMBER) { + for (i = 0; i < stats->files_changed; ++i) { + if ((delta = git_diff_get_delta(stats->diff, i)) == NULL) + continue; + + error = diff_file_stats_number_to_buf( + out, delta, &stats->filestats[i]); + if (error < 0) + return error; + } + } + + if (format & GIT_DIFF_STATS_FULL) { + if (width > 0) { + if (width > stats->max_name + stats->max_digits + 5) + width -= (stats->max_name + stats->max_digits + 5); + if (width < STATS_FULL_MIN_SCALE) + width = STATS_FULL_MIN_SCALE; + } + if (width > stats->max_filestat) + width = 0; + + for (i = 0; i < stats->files_changed; ++i) { + if ((delta = git_diff_get_delta(stats->diff, i)) == NULL) + continue; + + error = diff_file_stats_full_to_buf( + out, delta, &stats->filestats[i], stats, width); + if (error < 0) + return error; + } + } + + if (format & GIT_DIFF_STATS_FULL || format & GIT_DIFF_STATS_SHORT) { + git_str_printf( + out, " %" PRIuZ " file%s changed", + stats->files_changed, stats->files_changed != 1 ? "s" : ""); + + if (stats->insertions || stats->deletions == 0) + git_str_printf( + out, ", %" PRIuZ " insertion%s(+)", + stats->insertions, stats->insertions != 1 ? "s" : ""); + + if (stats->deletions || stats->insertions == 0) + git_str_printf( + out, ", %" PRIuZ " deletion%s(-)", + stats->deletions, stats->deletions != 1 ? "s" : ""); + + git_str_putc(out, '\n'); + + if (git_str_oom(out)) + return -1; + } + + if (format & GIT_DIFF_STATS_INCLUDE_SUMMARY) { + for (i = 0; i < stats->files_changed; ++i) { + if ((delta = git_diff_get_delta(stats->diff, i)) == NULL) + continue; + + error = diff_file_stats_summary_to_buf(out, delta); + if (error < 0) + return error; + } + } + + return error; +} + +void git_diff_stats_free(git_diff_stats *stats) +{ + if (stats == NULL) + return; + + git_diff_free(stats->diff); /* bumped refcount in constructor */ + git__free(stats->filestats); + git__free(stats); +} diff --git a/src/libgit2/diff_stats.h b/src/libgit2/diff_stats.h new file mode 100644 index 0000000..c71862b --- /dev/null +++ b/src/libgit2/diff_stats.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_diff_stats_h__ +#define INCLUDE_diff_stats_h__ + +#include "common.h" + +int git_diff__stats_to_buf( + git_str *out, + const git_diff_stats *stats, + git_diff_stats_format_t format, + size_t width); + +#endif diff --git a/src/libgit2/diff_tform.c b/src/libgit2/diff_tform.c new file mode 100644 index 0000000..4a156c7 --- /dev/null +++ b/src/libgit2/diff_tform.c @@ -0,0 +1,1125 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "diff_tform.h" + +#include "git2/config.h" +#include "git2/blob.h" +#include "git2/sys/hashsig.h" + +#include "diff.h" +#include "diff_generate.h" +#include "fs_path.h" +#include "futils.h" +#include "config.h" + +git_diff_delta *git_diff__delta_dup( + const git_diff_delta *d, git_pool *pool) +{ + git_diff_delta *delta = git__malloc(sizeof(git_diff_delta)); + if (!delta) + return NULL; + + memcpy(delta, d, sizeof(git_diff_delta)); + GIT_DIFF_FLAG__CLEAR_INTERNAL(delta->flags); + + if (d->old_file.path != NULL) { + delta->old_file.path = git_pool_strdup(pool, d->old_file.path); + if (delta->old_file.path == NULL) + goto fail; + } + + if (d->new_file.path != d->old_file.path && d->new_file.path != NULL) { + delta->new_file.path = git_pool_strdup(pool, d->new_file.path); + if (delta->new_file.path == NULL) + goto fail; + } else { + delta->new_file.path = delta->old_file.path; + } + + return delta; + +fail: + git__free(delta); + return NULL; +} + +git_diff_delta *git_diff__merge_like_cgit( + const git_diff_delta *a, + const git_diff_delta *b, + git_pool *pool) +{ + git_diff_delta *dup; + + /* Emulate C git for merging two diffs (a la 'git diff '). + * + * When C git does a diff between the work dir and a tree, it actually + * diffs with the index but uses the workdir contents. This emulates + * those choices so we can emulate the type of diff. + * + * We have three file descriptions here, let's call them: + * f1 = a->old_file + * f2 = a->new_file AND b->old_file + * f3 = b->new_file + */ + + /* If one of the diffs is a conflict, just dup it */ + if (b->status == GIT_DELTA_CONFLICTED) + return git_diff__delta_dup(b, pool); + if (a->status == GIT_DELTA_CONFLICTED) + return git_diff__delta_dup(a, pool); + + /* if f2 == f3 or f2 is deleted, then just dup the 'a' diff */ + if (b->status == GIT_DELTA_UNMODIFIED || a->status == GIT_DELTA_DELETED) + return git_diff__delta_dup(a, pool); + + /* otherwise, base this diff on the 'b' diff */ + if ((dup = git_diff__delta_dup(b, pool)) == NULL) + return NULL; + + /* If 'a' status is uninteresting, then we're done */ + if (a->status == GIT_DELTA_UNMODIFIED || + a->status == GIT_DELTA_UNTRACKED || + a->status == GIT_DELTA_UNREADABLE) + return dup; + + GIT_ASSERT_WITH_RETVAL(b->status != GIT_DELTA_UNMODIFIED, NULL); + + /* A cgit exception is that the diff of a file that is only in the + * index (i.e. not in HEAD nor workdir) is given as empty. + */ + if (dup->status == GIT_DELTA_DELETED) { + if (a->status == GIT_DELTA_ADDED) { + dup->status = GIT_DELTA_UNMODIFIED; + dup->nfiles = 2; + } + /* else don't overwrite DELETE status */ + } else { + dup->status = a->status; + dup->nfiles = a->nfiles; + } + + git_oid_cpy(&dup->old_file.id, &a->old_file.id); + dup->old_file.mode = a->old_file.mode; + dup->old_file.size = a->old_file.size; + dup->old_file.flags = a->old_file.flags; + + return dup; +} + +int git_diff__merge( + git_diff *onto, const git_diff *from, git_diff__merge_cb cb) +{ + int error = 0; + git_pool onto_pool; + git_vector onto_new; + git_diff_delta *delta; + bool ignore_case, reversed; + unsigned int i, j; + + GIT_ASSERT_ARG(onto); + GIT_ASSERT_ARG(from); + + if (!from->deltas.length) + return 0; + + ignore_case = ((onto->opts.flags & GIT_DIFF_IGNORE_CASE) != 0); + reversed = ((onto->opts.flags & GIT_DIFF_REVERSE) != 0); + + if (ignore_case != ((from->opts.flags & GIT_DIFF_IGNORE_CASE) != 0) || + reversed != ((from->opts.flags & GIT_DIFF_REVERSE) != 0)) { + git_error_set(GIT_ERROR_INVALID, + "attempt to merge diffs created with conflicting options"); + return -1; + } + + if (git_vector_init(&onto_new, onto->deltas.length, git_diff_delta__cmp) < 0 || + git_pool_init(&onto_pool, 1) < 0) + return -1; + + for (i = 0, j = 0; i < onto->deltas.length || j < from->deltas.length; ) { + git_diff_delta *o = GIT_VECTOR_GET(&onto->deltas, i); + const git_diff_delta *f = GIT_VECTOR_GET(&from->deltas, j); + int cmp = !f ? -1 : !o ? 1 : + STRCMP_CASESELECT(ignore_case, o->old_file.path, f->old_file.path); + + if (cmp < 0) { + delta = git_diff__delta_dup(o, &onto_pool); + i++; + } else if (cmp > 0) { + delta = git_diff__delta_dup(f, &onto_pool); + j++; + } else { + const git_diff_delta *left = reversed ? f : o; + const git_diff_delta *right = reversed ? o : f; + + delta = cb(left, right, &onto_pool); + i++; + j++; + } + + /* the ignore rules for the target may not match the source + * or the result of a merged delta could be skippable... + */ + if (delta && git_diff_delta__should_skip(&onto->opts, delta)) { + git__free(delta); + continue; + } + + if ((error = !delta ? -1 : git_vector_insert(&onto_new, delta)) < 0) + break; + } + + if (!error) { + git_vector_swap(&onto->deltas, &onto_new); + git_pool_swap(&onto->pool, &onto_pool); + + if ((onto->opts.flags & GIT_DIFF_REVERSE) != 0) + onto->old_src = from->old_src; + else + onto->new_src = from->new_src; + + /* prefix strings also come from old pool, so recreate those.*/ + onto->opts.old_prefix = + git_pool_strdup_safe(&onto->pool, onto->opts.old_prefix); + onto->opts.new_prefix = + git_pool_strdup_safe(&onto->pool, onto->opts.new_prefix); + } + + git_vector_free_deep(&onto_new); + git_pool_clear(&onto_pool); + + return error; +} + +int git_diff_merge(git_diff *onto, const git_diff *from) +{ + return git_diff__merge(onto, from, git_diff__merge_like_cgit); +} + +int git_diff_find_similar__hashsig_for_file( + void **out, const git_diff_file *f, const char *path, void *p) +{ + git_hashsig_option_t opt = (git_hashsig_option_t)(intptr_t)p; + + GIT_UNUSED(f); + return git_hashsig_create_fromfile((git_hashsig **)out, path, opt); +} + +int git_diff_find_similar__hashsig_for_buf( + void **out, const git_diff_file *f, const char *buf, size_t len, void *p) +{ + git_hashsig_option_t opt = (git_hashsig_option_t)(intptr_t)p; + + GIT_UNUSED(f); + return git_hashsig_create((git_hashsig **)out, buf, len, opt); +} + +void git_diff_find_similar__hashsig_free(void *sig, void *payload) +{ + GIT_UNUSED(payload); + git_hashsig_free(sig); +} + +int git_diff_find_similar__calc_similarity( + int *score, void *siga, void *sigb, void *payload) +{ + int error; + + GIT_UNUSED(payload); + error = git_hashsig_compare(siga, sigb); + if (error < 0) + return error; + + *score = error; + return 0; +} + +#define DEFAULT_THRESHOLD 50 +#define DEFAULT_BREAK_REWRITE_THRESHOLD 60 +#define DEFAULT_RENAME_LIMIT 1000 + +static int normalize_find_opts( + git_diff *diff, + git_diff_find_options *opts, + const git_diff_find_options *given) +{ + git_config *cfg = NULL; + git_hashsig_option_t hashsig_opts; + + GIT_ERROR_CHECK_VERSION(given, GIT_DIFF_FIND_OPTIONS_VERSION, "git_diff_find_options"); + + if (diff->repo != NULL && + git_repository_config__weakptr(&cfg, diff->repo) < 0) + return -1; + + if (given) + memcpy(opts, given, sizeof(*opts)); + + if (!given || + (given->flags & GIT_DIFF_FIND_ALL) == GIT_DIFF_FIND_BY_CONFIG) + { + if (cfg) { + char *rule = + git_config__get_string_force(cfg, "diff.renames", "true"); + int boolval; + + if (!git__parse_bool(&boolval, rule) && !boolval) + /* don't set FIND_RENAMES if bool value is false */; + else if (!strcasecmp(rule, "copies") || !strcasecmp(rule, "copy")) + opts->flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES; + else + opts->flags |= GIT_DIFF_FIND_RENAMES; + + git__free(rule); + } else { + /* set default flag */ + opts->flags |= GIT_DIFF_FIND_RENAMES; + } + } + + /* some flags imply others */ + + if (opts->flags & GIT_DIFF_FIND_EXACT_MATCH_ONLY) { + /* if we are only looking for exact matches, then don't turn + * MODIFIED items into ADD/DELETE pairs because it's too picky + */ + opts->flags &= ~(GIT_DIFF_FIND_REWRITES | GIT_DIFF_BREAK_REWRITES); + + /* similarly, don't look for self-rewrites to split */ + opts->flags &= ~GIT_DIFF_FIND_RENAMES_FROM_REWRITES; + } + + if (opts->flags & GIT_DIFF_FIND_RENAMES_FROM_REWRITES) + opts->flags |= GIT_DIFF_FIND_RENAMES; + + if (opts->flags & GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED) + opts->flags |= GIT_DIFF_FIND_COPIES; + + if (opts->flags & GIT_DIFF_BREAK_REWRITES) + opts->flags |= GIT_DIFF_FIND_REWRITES; + +#define USE_DEFAULT(X) ((X) == 0 || (X) > 100) + + if (USE_DEFAULT(opts->rename_threshold)) + opts->rename_threshold = DEFAULT_THRESHOLD; + + if (USE_DEFAULT(opts->rename_from_rewrite_threshold)) + opts->rename_from_rewrite_threshold = DEFAULT_THRESHOLD; + + if (USE_DEFAULT(opts->copy_threshold)) + opts->copy_threshold = DEFAULT_THRESHOLD; + + if (USE_DEFAULT(opts->break_rewrite_threshold)) + opts->break_rewrite_threshold = DEFAULT_BREAK_REWRITE_THRESHOLD; + +#undef USE_DEFAULT + + if (!opts->rename_limit) { + if (cfg) { + opts->rename_limit = git_config__get_int_force( + cfg, "diff.renamelimit", DEFAULT_RENAME_LIMIT); + } + + if (opts->rename_limit <= 0) + opts->rename_limit = DEFAULT_RENAME_LIMIT; + } + + /* assign the internal metric with whitespace flag as payload */ + if (!opts->metric) { + opts->metric = git__malloc(sizeof(git_diff_similarity_metric)); + GIT_ERROR_CHECK_ALLOC(opts->metric); + + opts->metric->file_signature = git_diff_find_similar__hashsig_for_file; + opts->metric->buffer_signature = git_diff_find_similar__hashsig_for_buf; + opts->metric->free_signature = git_diff_find_similar__hashsig_free; + opts->metric->similarity = git_diff_find_similar__calc_similarity; + + if (opts->flags & GIT_DIFF_FIND_IGNORE_WHITESPACE) + hashsig_opts = GIT_HASHSIG_IGNORE_WHITESPACE; + else if (opts->flags & GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE) + hashsig_opts = GIT_HASHSIG_NORMAL; + else + hashsig_opts = GIT_HASHSIG_SMART_WHITESPACE; + hashsig_opts |= GIT_HASHSIG_ALLOW_SMALL_FILES; + opts->metric->payload = (void *)hashsig_opts; + } + + return 0; +} + +static int insert_delete_side_of_split( + git_diff *diff, git_vector *onto, const git_diff_delta *delta) +{ + /* make new record for DELETED side of split */ + git_diff_delta *deleted = git_diff__delta_dup(delta, &diff->pool); + GIT_ERROR_CHECK_ALLOC(deleted); + + deleted->status = GIT_DELTA_DELETED; + deleted->nfiles = 1; + memset(&deleted->new_file, 0, sizeof(deleted->new_file)); + deleted->new_file.path = deleted->old_file.path; + deleted->new_file.flags |= GIT_DIFF_FLAG_VALID_ID; + git_oid_clear(&deleted->new_file.id, diff->opts.oid_type); + + return git_vector_insert(onto, deleted); +} + +static int apply_splits_and_deletes( + git_diff *diff, size_t expected_size, bool actually_split) +{ + git_vector onto = GIT_VECTOR_INIT; + size_t i; + git_diff_delta *delta; + + if (git_vector_init(&onto, expected_size, diff->deltas._cmp) < 0) + return -1; + + /* build new delta list without TO_DELETE and splitting TO_SPLIT */ + git_vector_foreach(&diff->deltas, i, delta) { + if ((delta->flags & GIT_DIFF_FLAG__TO_DELETE) != 0) + continue; + + if ((delta->flags & GIT_DIFF_FLAG__TO_SPLIT) != 0 && actually_split) { + delta->similarity = 0; + + if (insert_delete_side_of_split(diff, &onto, delta) < 0) + goto on_error; + + if (diff->new_src == GIT_ITERATOR_WORKDIR) + delta->status = GIT_DELTA_UNTRACKED; + else + delta->status = GIT_DELTA_ADDED; + delta->nfiles = 1; + memset(&delta->old_file, 0, sizeof(delta->old_file)); + delta->old_file.path = delta->new_file.path; + delta->old_file.flags |= GIT_DIFF_FLAG_VALID_ID; + git_oid_clear(&delta->old_file.id, diff->opts.oid_type); + } + + /* clean up delta before inserting into new list */ + GIT_DIFF_FLAG__CLEAR_INTERNAL(delta->flags); + + if (delta->status != GIT_DELTA_COPIED && + delta->status != GIT_DELTA_RENAMED && + (delta->status != GIT_DELTA_MODIFIED || actually_split)) + delta->similarity = 0; + + /* insert into new list */ + if (git_vector_insert(&onto, delta) < 0) + goto on_error; + } + + /* cannot return an error past this point */ + + /* free deltas from old list that didn't make it to the new one */ + git_vector_foreach(&diff->deltas, i, delta) { + if ((delta->flags & GIT_DIFF_FLAG__TO_DELETE) != 0) + git__free(delta); + } + + /* swap new delta list into place */ + git_vector_swap(&diff->deltas, &onto); + git_vector_free(&onto); + git_vector_sort(&diff->deltas); + + return 0; + +on_error: + git_vector_free_deep(&onto); + + return -1; +} + +GIT_INLINE(git_diff_file *) similarity_get_file(git_diff *diff, size_t idx) +{ + git_diff_delta *delta = git_vector_get(&diff->deltas, idx / 2); + return (idx & 1) ? &delta->new_file : &delta->old_file; +} + +typedef struct { + size_t idx; + git_iterator_t src; + git_repository *repo; + git_diff_file *file; + git_str data; + git_odb_object *odb_obj; + git_blob *blob; +} similarity_info; + +static int similarity_init( + similarity_info *info, git_diff *diff, size_t file_idx) +{ + info->idx = file_idx; + info->src = (file_idx & 1) ? diff->new_src : diff->old_src; + info->repo = diff->repo; + info->file = similarity_get_file(diff, file_idx); + info->odb_obj = NULL; + info->blob = NULL; + git_str_init(&info->data, 0); + + if ((info->file->flags & GIT_DIFF_FLAG_VALID_SIZE) || + info->src == GIT_ITERATOR_WORKDIR) + return 0; + + return git_diff_file__resolve_zero_size( + info->file, &info->odb_obj, info->repo); +} + +static int similarity_sig( + similarity_info *info, + const git_diff_find_options *opts, + void **cache) +{ + int error = 0; + git_diff_file *file = info->file; + + if (info->src == GIT_ITERATOR_WORKDIR) { + if ((error = git_repository_workdir_path( + &info->data, info->repo, file->path)) < 0) + return error; + + /* if path is not a regular file, just skip this item */ + if (!git_fs_path_isfile(info->data.ptr)) + return 0; + + /* TODO: apply wd-to-odb filters to file data if necessary */ + + error = opts->metric->file_signature( + &cache[info->idx], info->file, + info->data.ptr, opts->metric->payload); + } else { + /* if we didn't initially know the size, we might have an odb_obj + * around from earlier, so convert that, otherwise load the blob now + */ + if (info->odb_obj != NULL) + error = git_object__from_odb_object( + (git_object **)&info->blob, info->repo, + info->odb_obj, GIT_OBJECT_BLOB); + else + error = git_blob_lookup(&info->blob, info->repo, &file->id); + + if (error < 0) { + /* if lookup fails, just skip this item in similarity calc */ + git_error_clear(); + } else { + size_t sz; + + /* index size may not be actual blob size if filtered */ + if (file->size != git_blob_rawsize(info->blob)) + file->size = git_blob_rawsize(info->blob); + + sz = git__is_sizet(file->size) ? (size_t)file->size : (size_t)-1; + + error = opts->metric->buffer_signature( + &cache[info->idx], info->file, + git_blob_rawcontent(info->blob), sz, opts->metric->payload); + } + } + + return error; +} + +static void similarity_unload(similarity_info *info) +{ + if (info->odb_obj) + git_odb_object_free(info->odb_obj); + + if (info->blob) + git_blob_free(info->blob); + else + git_str_dispose(&info->data); +} + +#define FLAG_SET(opts,flag_name) (((opts)->flags & flag_name) != 0) + +/* - score < 0 means files cannot be compared + * - score >= 100 means files are exact match + * - score == 0 means files are completely different + */ +static int similarity_measure( + int *score, + git_diff *diff, + const git_diff_find_options *opts, + void **cache, + size_t a_idx, + size_t b_idx) +{ + git_diff_file *a_file = similarity_get_file(diff, a_idx); + git_diff_file *b_file = similarity_get_file(diff, b_idx); + bool exact_match = FLAG_SET(opts, GIT_DIFF_FIND_EXACT_MATCH_ONLY); + int error = 0; + similarity_info a_info, b_info; + + *score = -1; + + /* don't try to compare things that aren't files */ + if (!GIT_MODE_ISBLOB(a_file->mode) || !GIT_MODE_ISBLOB(b_file->mode)) + return 0; + + /* if exact match is requested, force calculation of missing OIDs now */ + if (exact_match) { + if (git_oid_is_zero(&a_file->id) && + diff->old_src == GIT_ITERATOR_WORKDIR && + !git_diff__oid_for_file(&a_file->id, + diff, a_file->path, a_file->mode, a_file->size)) + a_file->flags |= GIT_DIFF_FLAG_VALID_ID; + + if (git_oid_is_zero(&b_file->id) && + diff->new_src == GIT_ITERATOR_WORKDIR && + !git_diff__oid_for_file(&b_file->id, + diff, b_file->path, b_file->mode, b_file->size)) + b_file->flags |= GIT_DIFF_FLAG_VALID_ID; + } + + /* check OID match as a quick test */ + if (git_oid__cmp(&a_file->id, &b_file->id) == 0) { + *score = 100; + return 0; + } + + /* don't calculate signatures if we are doing exact match */ + if (exact_match) { + *score = 0; + return 0; + } + + memset(&a_info, 0, sizeof(a_info)); + memset(&b_info, 0, sizeof(b_info)); + + /* set up similarity data (will try to update missing file sizes) */ + if (!cache[a_idx] && (error = similarity_init(&a_info, diff, a_idx)) < 0) + return error; + if (!cache[b_idx] && (error = similarity_init(&b_info, diff, b_idx)) < 0) + goto cleanup; + + /* check if file sizes are nowhere near each other */ + if (a_file->size > 127 && + b_file->size > 127 && + (a_file->size > (b_file->size << 3) || + b_file->size > (a_file->size << 3))) + goto cleanup; + + /* update signature cache if needed */ + if (!cache[a_idx]) { + if ((error = similarity_sig(&a_info, opts, cache)) < 0) + goto cleanup; + } + if (!cache[b_idx]) { + if ((error = similarity_sig(&b_info, opts, cache)) < 0) + goto cleanup; + } + + /* calculate similarity provided that the metric choose to process + * both the a and b files (some may not if file is too big, etc). + */ + if (cache[a_idx] && cache[b_idx]) + error = opts->metric->similarity( + score, cache[a_idx], cache[b_idx], opts->metric->payload); + +cleanup: + similarity_unload(&a_info); + similarity_unload(&b_info); + + return error; +} + +static int calc_self_similarity( + git_diff *diff, + const git_diff_find_options *opts, + size_t delta_idx, + void **cache) +{ + int error, similarity = -1; + git_diff_delta *delta = GIT_VECTOR_GET(&diff->deltas, delta_idx); + + if ((delta->flags & GIT_DIFF_FLAG__HAS_SELF_SIMILARITY) != 0) + return 0; + + error = similarity_measure( + &similarity, diff, opts, cache, 2 * delta_idx, 2 * delta_idx + 1); + if (error < 0) + return error; + + if (similarity >= 0) { + delta->similarity = (uint16_t)similarity; + delta->flags |= GIT_DIFF_FLAG__HAS_SELF_SIMILARITY; + } + + return 0; +} + +static bool is_rename_target( + git_diff *diff, + const git_diff_find_options *opts, + size_t delta_idx, + void **cache) +{ + git_diff_delta *delta = GIT_VECTOR_GET(&diff->deltas, delta_idx); + + /* skip things that aren't plain blobs */ + if (!GIT_MODE_ISBLOB(delta->new_file.mode)) + return false; + + /* only consider ADDED, RENAMED, COPIED, and split MODIFIED as + * targets; maybe include UNTRACKED if requested. + */ + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + case GIT_DELTA_DELETED: + case GIT_DELTA_IGNORED: + case GIT_DELTA_CONFLICTED: + return false; + + case GIT_DELTA_MODIFIED: + if (!FLAG_SET(opts, GIT_DIFF_FIND_REWRITES) && + !FLAG_SET(opts, GIT_DIFF_FIND_RENAMES_FROM_REWRITES)) + return false; + + if (calc_self_similarity(diff, opts, delta_idx, cache) < 0) + return false; + + if (FLAG_SET(opts, GIT_DIFF_BREAK_REWRITES) && + delta->similarity < opts->break_rewrite_threshold) { + delta->flags |= GIT_DIFF_FLAG__TO_SPLIT; + break; + } + if (FLAG_SET(opts, GIT_DIFF_FIND_RENAMES_FROM_REWRITES) && + delta->similarity < opts->rename_from_rewrite_threshold) { + delta->flags |= GIT_DIFF_FLAG__TO_SPLIT; + break; + } + + return false; + + case GIT_DELTA_UNTRACKED: + if (!FLAG_SET(opts, GIT_DIFF_FIND_FOR_UNTRACKED)) + return false; + break; + + default: /* all other status values should be checked */ + break; + } + + delta->flags |= GIT_DIFF_FLAG__IS_RENAME_TARGET; + return true; +} + +static bool is_rename_source( + git_diff *diff, + const git_diff_find_options *opts, + size_t delta_idx, + void **cache) +{ + git_diff_delta *delta = GIT_VECTOR_GET(&diff->deltas, delta_idx); + + /* skip things that aren't blobs */ + if (!GIT_MODE_ISBLOB(delta->old_file.mode)) + return false; + + switch (delta->status) { + case GIT_DELTA_ADDED: + case GIT_DELTA_UNTRACKED: + case GIT_DELTA_UNREADABLE: + case GIT_DELTA_IGNORED: + case GIT_DELTA_CONFLICTED: + return false; + + case GIT_DELTA_DELETED: + case GIT_DELTA_TYPECHANGE: + break; + + case GIT_DELTA_UNMODIFIED: + if (!FLAG_SET(opts, GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED)) + return false; + if (FLAG_SET(opts, GIT_DIFF_FIND_REMOVE_UNMODIFIED)) + delta->flags |= GIT_DIFF_FLAG__TO_DELETE; + break; + + default: /* MODIFIED, RENAMED, COPIED */ + /* if we're finding copies, this could be a source */ + if (FLAG_SET(opts, GIT_DIFF_FIND_COPIES)) + break; + + /* otherwise, this is only a source if we can split it */ + if (!FLAG_SET(opts, GIT_DIFF_FIND_REWRITES) && + !FLAG_SET(opts, GIT_DIFF_FIND_RENAMES_FROM_REWRITES)) + return false; + + if (calc_self_similarity(diff, opts, delta_idx, cache) < 0) + return false; + + if (FLAG_SET(opts, GIT_DIFF_BREAK_REWRITES) && + delta->similarity < opts->break_rewrite_threshold) { + delta->flags |= GIT_DIFF_FLAG__TO_SPLIT; + break; + } + + if (FLAG_SET(opts, GIT_DIFF_FIND_RENAMES_FROM_REWRITES) && + delta->similarity < opts->rename_from_rewrite_threshold) + break; + + return false; + } + + delta->flags |= GIT_DIFF_FLAG__IS_RENAME_SOURCE; + return true; +} + +GIT_INLINE(bool) delta_is_split(git_diff_delta *delta) +{ + return (delta->status == GIT_DELTA_TYPECHANGE || + (delta->flags & GIT_DIFF_FLAG__TO_SPLIT) != 0); +} + +GIT_INLINE(bool) delta_is_new_only(git_diff_delta *delta) +{ + return (delta->status == GIT_DELTA_ADDED || + delta->status == GIT_DELTA_UNTRACKED || + delta->status == GIT_DELTA_UNREADABLE || + delta->status == GIT_DELTA_IGNORED); +} + +GIT_INLINE(void) delta_make_rename( + git_diff_delta *to, const git_diff_delta *from, uint16_t similarity) +{ + to->status = GIT_DELTA_RENAMED; + to->similarity = similarity; + to->nfiles = 2; + memcpy(&to->old_file, &from->old_file, sizeof(to->old_file)); + to->flags &= ~GIT_DIFF_FLAG__TO_SPLIT; +} + +typedef struct { + size_t idx; + uint16_t similarity; +} diff_find_match; + +int git_diff_find_similar( + git_diff *diff, + const git_diff_find_options *given_opts) +{ + size_t s, t; + int error = 0, result; + uint16_t similarity; + git_diff_delta *src, *tgt; + git_diff_find_options opts = GIT_DIFF_FIND_OPTIONS_INIT; + size_t num_deltas, num_srcs = 0, num_tgts = 0; + size_t tried_srcs = 0, tried_tgts = 0; + size_t num_rewrites = 0, num_updates = 0, num_bumped = 0; + size_t sigcache_size; + void **sigcache = NULL; /* cache of similarity metric file signatures */ + diff_find_match *tgt2src = NULL; + diff_find_match *src2tgt = NULL; + diff_find_match *tgt2src_copy = NULL; + diff_find_match *best_match; + git_diff_file swap; + + GIT_ASSERT_ARG(diff); + + if ((error = normalize_find_opts(diff, &opts, given_opts)) < 0) + return error; + + num_deltas = diff->deltas.length; + + /* TODO: maybe abort if deltas.length > rename_limit ??? */ + if (!num_deltas || !git__is_uint32(num_deltas)) + goto cleanup; + + /* No flags set; nothing to do */ + if ((opts.flags & GIT_DIFF_FIND_ALL) == 0) + goto cleanup; + + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&sigcache_size, num_deltas, 2); + sigcache = git__calloc(sigcache_size, sizeof(void *)); + GIT_ERROR_CHECK_ALLOC(sigcache); + + /* Label rename sources and targets + * + * This will also set self-similarity scores for MODIFIED files and + * mark them for splitting if break-rewrites is enabled + */ + git_vector_foreach(&diff->deltas, t, tgt) { + if (is_rename_source(diff, &opts, t, sigcache)) + ++num_srcs; + + if (is_rename_target(diff, &opts, t, sigcache)) + ++num_tgts; + + if ((tgt->flags & GIT_DIFF_FLAG__TO_SPLIT) != 0) + num_rewrites++; + } + + /* if there are no candidate srcs or tgts, we're done */ + if (!num_srcs || !num_tgts) + goto cleanup; + + src2tgt = git__calloc(num_deltas, sizeof(diff_find_match)); + GIT_ERROR_CHECK_ALLOC(src2tgt); + tgt2src = git__calloc(num_deltas, sizeof(diff_find_match)); + GIT_ERROR_CHECK_ALLOC(tgt2src); + + if (FLAG_SET(&opts, GIT_DIFF_FIND_COPIES)) { + tgt2src_copy = git__calloc(num_deltas, sizeof(diff_find_match)); + GIT_ERROR_CHECK_ALLOC(tgt2src_copy); + } + + /* + * Find best-fit matches for rename / copy candidates + */ + +find_best_matches: + tried_tgts = num_bumped = 0; + + git_vector_foreach(&diff->deltas, t, tgt) { + /* skip things that are not rename targets */ + if ((tgt->flags & GIT_DIFF_FLAG__IS_RENAME_TARGET) == 0) + continue; + + tried_srcs = 0; + + git_vector_foreach(&diff->deltas, s, src) { + /* skip things that are not rename sources */ + if ((src->flags & GIT_DIFF_FLAG__IS_RENAME_SOURCE) == 0) + continue; + + /* calculate similarity for this pair and find best match */ + if (s == t) + result = -1; /* don't measure self-similarity here */ + else if ((error = similarity_measure( + &result, diff, &opts, sigcache, 2 * s, 2 * t + 1)) < 0) + goto cleanup; + + if (result < 0) + continue; + similarity = (uint16_t)result; + + /* is this a better rename? */ + if (tgt2src[t].similarity < similarity && + src2tgt[s].similarity < similarity) + { + /* eject old mapping */ + if (src2tgt[s].similarity > 0) { + tgt2src[src2tgt[s].idx].similarity = 0; + num_bumped++; + } + if (tgt2src[t].similarity > 0) { + src2tgt[tgt2src[t].idx].similarity = 0; + num_bumped++; + } + + /* write new mapping */ + tgt2src[t].idx = s; + tgt2src[t].similarity = similarity; + src2tgt[s].idx = t; + src2tgt[s].similarity = similarity; + } + + /* keep best absolute match for copies */ + if (tgt2src_copy != NULL && + tgt2src_copy[t].similarity < similarity) + { + tgt2src_copy[t].idx = s; + tgt2src_copy[t].similarity = similarity; + } + + if (++tried_srcs >= num_srcs) + break; + + /* cap on maximum targets we'll examine (per "tgt" file) */ + if (tried_srcs > opts.rename_limit) + break; + } + + if (++tried_tgts >= num_tgts) + break; + } + + if (num_bumped > 0) /* try again if we bumped some items */ + goto find_best_matches; + + /* + * Rewrite the diffs with renames / copies + */ + + git_vector_foreach(&diff->deltas, t, tgt) { + /* skip things that are not rename targets */ + if ((tgt->flags & GIT_DIFF_FLAG__IS_RENAME_TARGET) == 0) + continue; + + /* check if this delta was the target of a similarity */ + if (tgt2src[t].similarity) + best_match = &tgt2src[t]; + else if (tgt2src_copy && tgt2src_copy[t].similarity) + best_match = &tgt2src_copy[t]; + else + continue; + + s = best_match->idx; + src = GIT_VECTOR_GET(&diff->deltas, s); + + /* possible scenarios: + * 1. from DELETE to ADD/UNTRACK/IGNORE = RENAME + * 2. from DELETE to SPLIT/TYPECHANGE = RENAME + DELETE + * 3. from SPLIT/TYPECHANGE to ADD/UNTRACK/IGNORE = ADD + RENAME + * 4. from SPLIT/TYPECHANGE to SPLIT/TYPECHANGE = RENAME + SPLIT + * 5. from OTHER to ADD/UNTRACK/IGNORE = OTHER + COPY + */ + + if (src->status == GIT_DELTA_DELETED) { + + if (delta_is_new_only(tgt)) { + + if (best_match->similarity < opts.rename_threshold) + continue; + + delta_make_rename(tgt, src, best_match->similarity); + + src->flags |= GIT_DIFF_FLAG__TO_DELETE; + num_rewrites++; + } else { + GIT_ASSERT(delta_is_split(tgt)); + + if (best_match->similarity < opts.rename_from_rewrite_threshold) + continue; + + memcpy(&swap, &tgt->old_file, sizeof(swap)); + + delta_make_rename(tgt, src, best_match->similarity); + num_rewrites--; + + GIT_ASSERT(src->status == GIT_DELTA_DELETED); + memcpy(&src->old_file, &swap, sizeof(src->old_file)); + memset(&src->new_file, 0, sizeof(src->new_file)); + src->new_file.path = src->old_file.path; + src->new_file.flags |= GIT_DIFF_FLAG_VALID_ID; + git_oid_clear(&src->new_file.id, diff->opts.oid_type); + + num_updates++; + + if (src2tgt[t].similarity > 0 && src2tgt[t].idx > t) { + /* what used to be at src t is now at src s */ + tgt2src[src2tgt[t].idx].idx = s; + } + } + } + + else if (delta_is_split(src)) { + + if (delta_is_new_only(tgt)) { + + if (best_match->similarity < opts.rename_threshold) + continue; + + delta_make_rename(tgt, src, best_match->similarity); + + src->status = (diff->new_src == GIT_ITERATOR_WORKDIR) ? + GIT_DELTA_UNTRACKED : GIT_DELTA_ADDED; + src->nfiles = 1; + memset(&src->old_file, 0, sizeof(src->old_file)); + src->old_file.path = src->new_file.path; + src->old_file.flags |= GIT_DIFF_FLAG_VALID_ID; + git_oid_clear(&src->old_file.id, diff->opts.oid_type); + + src->flags &= ~GIT_DIFF_FLAG__TO_SPLIT; + num_rewrites--; + + num_updates++; + } else { + GIT_ASSERT(delta_is_split(src)); + + if (best_match->similarity < opts.rename_from_rewrite_threshold) + continue; + + memcpy(&swap, &tgt->old_file, sizeof(swap)); + + delta_make_rename(tgt, src, best_match->similarity); + num_rewrites--; + num_updates++; + + memcpy(&src->old_file, &swap, sizeof(src->old_file)); + + /* if we've just swapped the new element into the correct + * place, clear the SPLIT and RENAME_TARGET flags + */ + if (tgt2src[s].idx == t && + tgt2src[s].similarity > + opts.rename_from_rewrite_threshold) { + src->status = GIT_DELTA_RENAMED; + src->similarity = tgt2src[s].similarity; + tgt2src[s].similarity = 0; + src->flags &= ~(GIT_DIFF_FLAG__TO_SPLIT | GIT_DIFF_FLAG__IS_RENAME_TARGET); + num_rewrites--; + } + /* otherwise, if we just overwrote a source, update mapping */ + else if (src2tgt[t].similarity > 0 && src2tgt[t].idx > t) { + /* what used to be at src t is now at src s */ + tgt2src[src2tgt[t].idx].idx = s; + } + + num_updates++; + } + } + + else if (FLAG_SET(&opts, GIT_DIFF_FIND_COPIES)) { + if (tgt2src_copy[t].similarity < opts.copy_threshold) + continue; + + /* always use best possible source for copy */ + best_match = &tgt2src_copy[t]; + src = GIT_VECTOR_GET(&diff->deltas, best_match->idx); + + if (delta_is_split(tgt)) { + error = insert_delete_side_of_split(diff, &diff->deltas, tgt); + if (error < 0) + goto cleanup; + num_rewrites--; + } + + if (!delta_is_split(tgt) && !delta_is_new_only(tgt)) + continue; + + tgt->status = GIT_DELTA_COPIED; + tgt->similarity = best_match->similarity; + tgt->nfiles = 2; + memcpy(&tgt->old_file, &src->old_file, sizeof(tgt->old_file)); + tgt->flags &= ~GIT_DIFF_FLAG__TO_SPLIT; + + num_updates++; + } + } + + /* + * Actually split and delete entries as needed + */ + + if (num_rewrites > 0 || num_updates > 0) + error = apply_splits_and_deletes( + diff, diff->deltas.length - num_rewrites, + FLAG_SET(&opts, GIT_DIFF_BREAK_REWRITES) && + !FLAG_SET(&opts, GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY)); + +cleanup: + git__free(tgt2src); + git__free(src2tgt); + git__free(tgt2src_copy); + + if (sigcache) { + for (t = 0; t < num_deltas * 2; ++t) { + if (sigcache[t] != NULL) + opts.metric->free_signature(sigcache[t], opts.metric->payload); + } + git__free(sigcache); + } + + if (!given_opts || !given_opts->metric) + git__free(opts.metric); + + return error; +} + +#undef FLAG_SET diff --git a/src/libgit2/diff_tform.h b/src/libgit2/diff_tform.h new file mode 100644 index 0000000..7abb8b3 --- /dev/null +++ b/src/libgit2/diff_tform.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_diff_tform_h__ +#define INCLUDE_diff_tform_h__ + +#include "common.h" + +#include "diff_file.h" + +extern int git_diff_find_similar__hashsig_for_file( + void **out, const git_diff_file *f, const char *path, void *p); + +extern int git_diff_find_similar__hashsig_for_buf( + void **out, const git_diff_file *f, const char *buf, size_t len, void *p); + +extern void git_diff_find_similar__hashsig_free(void *sig, void *payload); + +extern int git_diff_find_similar__calc_similarity( + int *score, void *siga, void *sigb, void *payload); + +#endif diff --git a/src/libgit2/diff_xdiff.c b/src/libgit2/diff_xdiff.c new file mode 100644 index 0000000..5f56c52 --- /dev/null +++ b/src/libgit2/diff_xdiff.c @@ -0,0 +1,261 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "diff_xdiff.h" + +#include "git2/errors.h" +#include "diff.h" +#include "diff_driver.h" +#include "patch_generate.h" +#include "utf8.h" + +static int git_xdiff_scan_int(const char **str, int *value) +{ + const char *scan = *str; + int v = 0, digits = 0; + /* find next digit */ + for (scan = *str; *scan && !git__isdigit(*scan); scan++); + /* parse next number */ + for (; git__isdigit(*scan); scan++, digits++) + v = (v * 10) + (*scan - '0'); + *str = scan; + *value = v; + return (digits > 0) ? 0 : -1; +} + +static int git_xdiff_parse_hunk(git_diff_hunk *hunk, const char *header) +{ + /* expect something of the form "@@ -%d[,%d] +%d[,%d] @@" */ + if (*header != '@') + goto fail; + if (git_xdiff_scan_int(&header, &hunk->old_start) < 0) + goto fail; + if (*header == ',') { + if (git_xdiff_scan_int(&header, &hunk->old_lines) < 0) + goto fail; + } else + hunk->old_lines = 1; + if (git_xdiff_scan_int(&header, &hunk->new_start) < 0) + goto fail; + if (*header == ',') { + if (git_xdiff_scan_int(&header, &hunk->new_lines) < 0) + goto fail; + } else + hunk->new_lines = 1; + if (hunk->old_start < 0 || hunk->new_start < 0) + goto fail; + + return 0; + +fail: + git_error_set(GIT_ERROR_INVALID, "malformed hunk header from xdiff"); + return -1; +} + +typedef struct { + git_xdiff_output *xo; + git_patch_generated *patch; + git_diff_hunk hunk; + int old_lineno, new_lineno; + mmfile_t xd_old_data, xd_new_data; +} git_xdiff_info; + +static int diff_update_lines( + git_xdiff_info *info, + git_diff_line *line, + const char *content, + size_t content_len) +{ + const char *scan = content, *scan_end = content + content_len; + + for (line->num_lines = 0; scan < scan_end; ++scan) + if (*scan == '\n') + ++line->num_lines; + + line->content = content; + line->content_len = content_len; + + /* expect " "/"-"/"+", then data */ + switch (line->origin) { + case GIT_DIFF_LINE_ADDITION: + case GIT_DIFF_LINE_DEL_EOFNL: + line->old_lineno = -1; + line->new_lineno = info->new_lineno; + info->new_lineno += (int)line->num_lines; + break; + case GIT_DIFF_LINE_DELETION: + case GIT_DIFF_LINE_ADD_EOFNL: + line->old_lineno = info->old_lineno; + line->new_lineno = -1; + info->old_lineno += (int)line->num_lines; + break; + case GIT_DIFF_LINE_CONTEXT: + case GIT_DIFF_LINE_CONTEXT_EOFNL: + line->old_lineno = info->old_lineno; + line->new_lineno = info->new_lineno; + info->old_lineno += (int)line->num_lines; + info->new_lineno += (int)line->num_lines; + break; + default: + git_error_set(GIT_ERROR_INVALID, "unknown diff line origin %02x", + (unsigned int)line->origin); + return -1; + } + + return 0; +} + +static int git_xdiff_cb(void *priv, mmbuffer_t *bufs, int len) +{ + git_xdiff_info *info = priv; + git_patch_generated *patch = info->patch; + const git_diff_delta *delta = patch->base.delta; + git_patch_generated_output *output = &info->xo->output; + git_diff_line line; + size_t buffer_len; + + if (len == 1) { + output->error = git_xdiff_parse_hunk(&info->hunk, bufs[0].ptr); + if (output->error < 0) + return output->error; + + info->hunk.header_len = bufs[0].size; + if (info->hunk.header_len >= sizeof(info->hunk.header)) + info->hunk.header_len = sizeof(info->hunk.header) - 1; + + /* Sanitize the hunk header in case there is invalid Unicode */ + buffer_len = git_utf8_valid_buf_length(bufs[0].ptr, info->hunk.header_len); + /* Sanitizing the hunk header may delete the newline, so add it back again if there is room */ + if (buffer_len < info->hunk.header_len) { + bufs[0].ptr[buffer_len] = '\n'; + buffer_len += 1; + info->hunk.header_len = buffer_len; + } + + memcpy(info->hunk.header, bufs[0].ptr, info->hunk.header_len); + info->hunk.header[info->hunk.header_len] = '\0'; + + if (output->hunk_cb != NULL && + (output->error = output->hunk_cb( + delta, &info->hunk, output->payload))) + return output->error; + + info->old_lineno = info->hunk.old_start; + info->new_lineno = info->hunk.new_start; + } + + if (len == 2 || len == 3) { + /* expect " "/"-"/"+", then data */ + line.origin = + (*bufs[0].ptr == '+') ? GIT_DIFF_LINE_ADDITION : + (*bufs[0].ptr == '-') ? GIT_DIFF_LINE_DELETION : + GIT_DIFF_LINE_CONTEXT; + + if (line.origin == GIT_DIFF_LINE_ADDITION) + line.content_offset = bufs[1].ptr - info->xd_new_data.ptr; + else if (line.origin == GIT_DIFF_LINE_DELETION) + line.content_offset = bufs[1].ptr - info->xd_old_data.ptr; + else + line.content_offset = -1; + + output->error = diff_update_lines( + info, &line, bufs[1].ptr, bufs[1].size); + + if (!output->error && output->data_cb != NULL) + output->error = output->data_cb( + delta, &info->hunk, &line, output->payload); + } + + if (len == 3 && !output->error) { + /* If we have a '+' and a third buf, then we have added a line + * without a newline and the old code had one, so DEL_EOFNL. + * If we have a '-' and a third buf, then we have removed a line + * with out a newline but added a blank line, so ADD_EOFNL. + */ + line.origin = + (*bufs[0].ptr == '+') ? GIT_DIFF_LINE_DEL_EOFNL : + (*bufs[0].ptr == '-') ? GIT_DIFF_LINE_ADD_EOFNL : + GIT_DIFF_LINE_CONTEXT_EOFNL; + + line.content_offset = -1; + + output->error = diff_update_lines( + info, &line, bufs[2].ptr, bufs[2].size); + + if (!output->error && output->data_cb != NULL) + output->error = output->data_cb( + delta, &info->hunk, &line, output->payload); + } + + return output->error; +} + +static int git_xdiff(git_patch_generated_output *output, git_patch_generated *patch) +{ + git_xdiff_output *xo = (git_xdiff_output *)output; + git_xdiff_info info; + git_diff_find_context_payload findctxt; + + memset(&info, 0, sizeof(info)); + info.patch = patch; + info.xo = xo; + + xo->callback.priv = &info; + + git_diff_find_context_init( + &xo->config.find_func, &findctxt, git_patch_generated_driver(patch)); + xo->config.find_func_priv = &findctxt; + + if (xo->config.find_func != NULL) + xo->config.flags |= XDL_EMIT_FUNCNAMES; + else + xo->config.flags &= ~XDL_EMIT_FUNCNAMES; + + /* TODO: check ofile.opts_flags to see if driver-specific per-file + * updates are needed to xo->params.flags + */ + + if (git_patch_generated_old_data(&info.xd_old_data.ptr, &info.xd_old_data.size, patch) < 0 || + git_patch_generated_new_data(&info.xd_new_data.ptr, &info.xd_new_data.size, patch) < 0) + return -1; + + xdl_diff(&info.xd_old_data, &info.xd_new_data, + &xo->params, &xo->config, &xo->callback); + + git_diff_find_context_clear(&findctxt); + + return xo->output.error; +} + +void git_xdiff_init(git_xdiff_output *xo, const git_diff_options *opts) +{ + uint32_t flags = opts ? opts->flags : 0; + + xo->output.diff_cb = git_xdiff; + + xo->config.ctxlen = opts ? opts->context_lines : 3; + xo->config.interhunkctxlen = opts ? opts->interhunk_lines : 0; + + if (flags & GIT_DIFF_IGNORE_WHITESPACE) + xo->params.flags |= XDF_WHITESPACE_FLAGS; + if (flags & GIT_DIFF_IGNORE_WHITESPACE_CHANGE) + xo->params.flags |= XDF_IGNORE_WHITESPACE_CHANGE; + if (flags & GIT_DIFF_IGNORE_WHITESPACE_EOL) + xo->params.flags |= XDF_IGNORE_WHITESPACE_AT_EOL; + if (flags & GIT_DIFF_INDENT_HEURISTIC) + xo->params.flags |= XDF_INDENT_HEURISTIC; + + if (flags & GIT_DIFF_PATIENCE) + xo->params.flags |= XDF_PATIENCE_DIFF; + if (flags & GIT_DIFF_MINIMAL) + xo->params.flags |= XDF_NEED_MINIMAL; + + if (flags & GIT_DIFF_IGNORE_BLANK_LINES) + xo->params.flags |= XDF_IGNORE_BLANK_LINES; + + xo->callback.out_line = git_xdiff_cb; +} diff --git a/src/libgit2/diff_xdiff.h b/src/libgit2/diff_xdiff.h new file mode 100644 index 0000000..327dc7c --- /dev/null +++ b/src/libgit2/diff_xdiff.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_diff_xdiff_h__ +#define INCLUDE_diff_xdiff_h__ + +#include "common.h" + +#include "diff.h" +#include "xdiff.h" +#include "patch_generate.h" + +/* xdiff cannot cope with large files. these files should not be passed to + * xdiff. callers should treat these large files as binary. + */ +#define GIT_XDIFF_MAX_SIZE (INT64_C(1024) * 1024 * 1023) + +/* A git_xdiff_output is a git_patch_generate_output with extra fields + * necessary to use libxdiff. Calling git_xdiff_init() will set the diff_cb + * field of the output to use xdiff to generate the diffs. + */ +typedef struct { + git_patch_generated_output output; + + xdemitconf_t config; + xpparam_t params; + xdemitcb_t callback; +} git_xdiff_output; + +void git_xdiff_init(git_xdiff_output *xo, const git_diff_options *opts); + +#endif diff --git a/src/libgit2/email.c b/src/libgit2/email.c new file mode 100644 index 0000000..8a10a12 --- /dev/null +++ b/src/libgit2/email.c @@ -0,0 +1,316 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "email.h" + +#include "common.h" +#include "buf.h" +#include "diff_generate.h" +#include "diff_stats.h" +#include "patch.h" +#include "date.h" + +#include "git2/email.h" +#include "git2/patch.h" +#include "git2/version.h" + +/* + * Git uses a "magic" timestamp to indicate that an email message + * is from `git format-patch` (or our equivalent). + */ +#define EMAIL_TIMESTAMP "Mon Sep 17 00:00:00 2001" + +GIT_INLINE(int) include_prefix( + size_t patch_count, + git_email_create_options *opts) +{ + return ((!opts->subject_prefix || *opts->subject_prefix) || + (opts->flags & GIT_EMAIL_CREATE_ALWAYS_NUMBER) != 0 || + opts->reroll_number || + (patch_count > 1 && !(opts->flags & GIT_EMAIL_CREATE_OMIT_NUMBERS))); +} + +static int append_prefix( + git_str *out, + size_t patch_idx, + size_t patch_count, + git_email_create_options *opts) +{ + const char *subject_prefix = opts->subject_prefix ? + opts->subject_prefix : "PATCH"; + + git_str_putc(out, '['); + + if (*subject_prefix) + git_str_puts(out, subject_prefix); + + if (opts->reroll_number) { + if (*subject_prefix) + git_str_putc(out, ' '); + + git_str_printf(out, "v%" PRIuZ, opts->reroll_number); + } + + if ((opts->flags & GIT_EMAIL_CREATE_ALWAYS_NUMBER) != 0 || + (patch_count > 1 && !(opts->flags & GIT_EMAIL_CREATE_OMIT_NUMBERS))) { + size_t start_number = opts->start_number ? + opts->start_number : 1; + + if (*subject_prefix || opts->reroll_number) + git_str_putc(out, ' '); + + git_str_printf(out, "%" PRIuZ "/%" PRIuZ, + patch_idx + (start_number - 1), + patch_count + (start_number - 1)); + } + + git_str_puts(out, "]"); + + return git_str_oom(out) ? -1 : 0; +} + +static int append_date( + git_str *out, + const git_time *date) +{ + int error; + + if ((error = git_str_printf(out, "Date: ")) == 0 && + (error = git_date_rfc2822_fmt(out, date->time, date->offset)) == 0) + error = git_str_putc(out, '\n'); + + return error; +} + +static int append_subject( + git_str *out, + size_t patch_idx, + size_t patch_count, + const char *summary, + git_email_create_options *opts) +{ + bool prefix = include_prefix(patch_count, opts); + size_t summary_len = summary ? strlen(summary) : 0; + int error; + + if (summary_len) { + const char *nl = strchr(summary, '\n'); + + if (nl) + summary_len = (nl - summary); + } + + if ((error = git_str_puts(out, "Subject: ")) < 0) + return error; + + if (prefix && + (error = append_prefix(out, patch_idx, patch_count, opts)) < 0) + return error; + + if (prefix && summary_len && (error = git_str_putc(out, ' ')) < 0) + return error; + + if (summary_len && + (error = git_str_put(out, summary, summary_len)) < 0) + return error; + + return git_str_putc(out, '\n'); +} + +static int append_header( + git_str *out, + size_t patch_idx, + size_t patch_count, + const git_oid *commit_id, + const char *summary, + const git_signature *author, + git_email_create_options *opts) +{ + char id[GIT_OID_MAX_HEXSIZE + 1]; + int error; + + git_oid_tostr(id, GIT_OID_MAX_HEXSIZE + 1, commit_id); + + if ((error = git_str_printf(out, "From %s %s\n", id, EMAIL_TIMESTAMP)) < 0 || + (error = git_str_printf(out, "From: %s <%s>\n", author->name, author->email)) < 0 || + (error = append_date(out, &author->when)) < 0 || + (error = append_subject(out, patch_idx, patch_count, summary, opts)) < 0) + return error; + + if ((error = git_str_putc(out, '\n')) < 0) + return error; + + return 0; +} + +static int append_body(git_str *out, const char *body) +{ + size_t body_len; + int error; + + if (!body) + return 0; + + body_len = strlen(body); + + if ((error = git_str_puts(out, body)) < 0) + return error; + + if (body_len && body[body_len - 1] != '\n') + error = git_str_putc(out, '\n'); + + return error; +} + +static int append_diffstat(git_str *out, git_diff *diff) +{ + git_diff_stats *stats = NULL; + unsigned int format_flags; + int error; + + format_flags = GIT_DIFF_STATS_FULL | GIT_DIFF_STATS_INCLUDE_SUMMARY; + + if ((error = git_diff_get_stats(&stats, diff)) == 0 && + (error = git_diff__stats_to_buf(out, stats, format_flags, 0)) == 0) + error = git_str_putc(out, '\n'); + + git_diff_stats_free(stats); + return error; +} + +static int append_patches(git_str *out, git_diff *diff) +{ + size_t i, deltas; + int error = 0; + + deltas = git_diff_num_deltas(diff); + + for (i = 0; i < deltas; ++i) { + git_patch *patch = NULL; + + if ((error = git_patch_from_diff(&patch, diff, i)) >= 0) + error = git_patch__to_buf(out, patch); + + git_patch_free(patch); + + if (error < 0) + break; + } + + return error; +} + +int git_email__append_from_diff( + git_str *out, + git_diff *diff, + size_t patch_idx, + size_t patch_count, + const git_oid *commit_id, + const char *summary, + const char *body, + const git_signature *author, + const git_email_create_options *given_opts) +{ + git_email_create_options opts = GIT_EMAIL_CREATE_OPTIONS_INIT; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(diff); + GIT_ASSERT_ARG(!patch_idx || patch_idx <= patch_count); + GIT_ASSERT_ARG(commit_id); + GIT_ASSERT_ARG(author); + + GIT_ERROR_CHECK_VERSION(given_opts, + GIT_EMAIL_CREATE_OPTIONS_VERSION, + "git_email_create_options"); + + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_email_create_options)); + + if ((error = append_header(out, patch_idx, patch_count, commit_id, summary, author, &opts)) == 0 && + (error = append_body(out, body)) == 0 && + (error = git_str_puts(out, "---\n")) == 0 && + (error = append_diffstat(out, diff)) == 0 && + (error = append_patches(out, diff)) == 0) + error = git_str_puts(out, "--\nlibgit2 " LIBGIT2_VERSION "\n\n"); + + return error; +} + +int git_email_create_from_diff( + git_buf *out, + git_diff *diff, + size_t patch_idx, + size_t patch_count, + const git_oid *commit_id, + const char *summary, + const char *body, + const git_signature *author, + const git_email_create_options *given_opts) +{ + git_str email = GIT_STR_INIT; + int error; + + git_buf_tostr(&email, out); + + error = git_email__append_from_diff(&email, diff, patch_idx, + patch_count, commit_id, summary, body, author, + given_opts); + + if (error == 0) + error = git_buf_fromstr(out, &email); + + git_str_dispose(&email); + return error; +} + +int git_email_create_from_commit( + git_buf *out, + git_commit *commit, + const git_email_create_options *given_opts) +{ + git_email_create_options opts = GIT_EMAIL_CREATE_OPTIONS_INIT; + git_diff *diff = NULL; + git_repository *repo; + git_diff_options *diff_opts; + git_diff_find_options *find_opts; + const git_signature *author; + const char *summary, *body; + const git_oid *commit_id; + int error = -1; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(commit); + + GIT_ERROR_CHECK_VERSION(given_opts, + GIT_EMAIL_CREATE_OPTIONS_VERSION, + "git_email_create_options"); + + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_email_create_options)); + + repo = git_commit_owner(commit); + author = git_commit_author(commit); + summary = git_commit_summary(commit); + body = git_commit_body(commit); + commit_id = git_commit_id(commit); + diff_opts = &opts.diff_opts; + find_opts = &opts.diff_find_opts; + + if ((error = git_diff__commit(&diff, repo, commit, diff_opts)) < 0) + goto done; + + if ((opts.flags & GIT_EMAIL_CREATE_NO_RENAMES) == 0 && + (error = git_diff_find_similar(diff, find_opts)) < 0) + goto done; + + error = git_email_create_from_diff(out, diff, 1, 1, commit_id, summary, body, author, &opts); + +done: + git_diff_free(diff); + return error; +} diff --git a/src/libgit2/email.h b/src/libgit2/email.h new file mode 100644 index 0000000..083e56d --- /dev/null +++ b/src/libgit2/email.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_email_h__ +#define INCLUDE_email_h__ + +#include "common.h" + +#include "git2/email.h" + +extern int git_email__append_from_diff( + git_str *out, + git_diff *diff, + size_t patch_idx, + size_t patch_count, + const git_oid *commit_id, + const char *summary, + const char *body, + const git_signature *author, + const git_email_create_options *given_opts); + +#endif diff --git a/src/libgit2/errors.c b/src/libgit2/errors.c new file mode 100644 index 0000000..2e58948 --- /dev/null +++ b/src/libgit2/errors.c @@ -0,0 +1,293 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "threadstate.h" +#include "posix.h" +#include "str.h" +#include "libgit2.h" + +/******************************************** + * New error handling + ********************************************/ + +static git_error oom_error = { + "Out of memory", + GIT_ERROR_NOMEMORY +}; + +static git_error uninitialized_error = { + "libgit2 has not been initialized; you must call git_libgit2_init", + GIT_ERROR_INVALID +}; + +static git_error tlsdata_error = { + "thread-local data initialization failure", + GIT_ERROR +}; + +static void set_error_from_buffer(int error_class) +{ + git_threadstate *threadstate = git_threadstate_get(); + git_error *error; + git_str *buf; + + if (!threadstate) + return; + + error = &threadstate->error_t; + buf = &threadstate->error_buf; + + error->message = buf->ptr; + error->klass = error_class; + + threadstate->last_error = error; +} + +static void set_error(int error_class, char *string) +{ + git_threadstate *threadstate = git_threadstate_get(); + git_str *buf; + + if (!threadstate) + return; + + buf = &threadstate->error_buf; + + git_str_clear(buf); + + if (string) { + git_str_puts(buf, string); + git__free(string); + } + + set_error_from_buffer(error_class); +} + +void git_error_set_oom(void) +{ + git_threadstate *threadstate = git_threadstate_get(); + + if (!threadstate) + return; + + threadstate->last_error = &oom_error; +} + +void git_error_set(int error_class, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + git_error_vset(error_class, fmt, ap); + va_end(ap); +} + +void git_error_vset(int error_class, const char *fmt, va_list ap) +{ +#ifdef GIT_WIN32 + DWORD win32_error_code = (error_class == GIT_ERROR_OS) ? GetLastError() : 0; +#endif + + git_threadstate *threadstate = git_threadstate_get(); + int error_code = (error_class == GIT_ERROR_OS) ? errno : 0; + git_str *buf; + + if (!threadstate) + return; + + buf = &threadstate->error_buf; + + git_str_clear(buf); + + if (fmt) { + git_str_vprintf(buf, fmt, ap); + if (error_class == GIT_ERROR_OS) + git_str_PUTS(buf, ": "); + } + + if (error_class == GIT_ERROR_OS) { +#ifdef GIT_WIN32 + char *win32_error = git_win32_get_error_message(win32_error_code); + if (win32_error) { + git_str_puts(buf, win32_error); + git__free(win32_error); + + SetLastError(0); + } + else +#endif + if (error_code) + git_str_puts(buf, strerror(error_code)); + + if (error_code) + errno = 0; + } + + if (!git_str_oom(buf)) + set_error_from_buffer(error_class); +} + +int git_error_set_str(int error_class, const char *string) +{ + git_threadstate *threadstate = git_threadstate_get(); + git_str *buf; + + GIT_ASSERT_ARG(string); + + if (!threadstate) + return -1; + + buf = &threadstate->error_buf; + + git_str_clear(buf); + git_str_puts(buf, string); + + if (git_str_oom(buf)) + return -1; + + set_error_from_buffer(error_class); + return 0; +} + +void git_error_clear(void) +{ + git_threadstate *threadstate = git_threadstate_get(); + + if (!threadstate) + return; + + if (threadstate->last_error != NULL) { + set_error(0, NULL); + threadstate->last_error = NULL; + } + + errno = 0; +#ifdef GIT_WIN32 + SetLastError(0); +#endif +} + +const git_error *git_error_last(void) +{ + git_threadstate *threadstate; + + /* If the library is not initialized, return a static error. */ + if (!git_libgit2_init_count()) + return &uninitialized_error; + + if ((threadstate = git_threadstate_get()) == NULL) + return &tlsdata_error; + + return threadstate->last_error; +} + +int git_error_state_capture(git_error_state *state, int error_code) +{ + git_threadstate *threadstate = git_threadstate_get(); + git_error *error; + git_str *error_buf; + + if (!threadstate) + return -1; + + error = threadstate->last_error; + error_buf = &threadstate->error_buf; + + memset(state, 0, sizeof(git_error_state)); + + if (!error_code) + return 0; + + state->error_code = error_code; + state->oom = (error == &oom_error); + + if (error) { + state->error_msg.klass = error->klass; + + if (state->oom) + state->error_msg.message = oom_error.message; + else + state->error_msg.message = git_str_detach(error_buf); + } + + git_error_clear(); + return error_code; +} + +int git_error_state_restore(git_error_state *state) +{ + int ret = 0; + + git_error_clear(); + + if (state && state->error_msg.message) { + if (state->oom) + git_error_set_oom(); + else + set_error(state->error_msg.klass, state->error_msg.message); + + ret = state->error_code; + memset(state, 0, sizeof(git_error_state)); + } + + return ret; +} + +void git_error_state_free(git_error_state *state) +{ + if (!state) + return; + + if (!state->oom) + git__free(state->error_msg.message); + + memset(state, 0, sizeof(git_error_state)); +} + +int git_error_system_last(void) +{ +#ifdef GIT_WIN32 + return GetLastError(); +#else + return errno; +#endif +} + +void git_error_system_set(int code) +{ +#ifdef GIT_WIN32 + SetLastError(code); +#else + errno = code; +#endif +} + +/* Deprecated error values and functions */ + +#ifndef GIT_DEPRECATE_HARD +const git_error *giterr_last(void) +{ + return git_error_last(); +} + +void giterr_clear(void) +{ + git_error_clear(); +} + +void giterr_set_str(int error_class, const char *string) +{ + git_error_set_str(error_class, string); +} + +void giterr_set_oom(void) +{ + git_error_set_oom(); +} +#endif diff --git a/src/libgit2/errors.h b/src/libgit2/errors.h new file mode 100644 index 0000000..772c7ba --- /dev/null +++ b/src/libgit2/errors.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_errors_h__ +#define INCLUDE_errors_h__ + +#include "common.h" + +/* + * `vprintf`-style formatting for the error message for this thread. + */ +void git_error_vset(int error_class, const char *fmt, va_list ap); + +/** + * Set error message for user callback if needed. + * + * If the error code in non-zero and no error message is set, this + * sets a generic error message. + * + * @return This always returns the `error_code` parameter. + */ +GIT_INLINE(int) git_error_set_after_callback_function( + int error_code, const char *action) +{ + if (error_code) { + const git_error *e = git_error_last(); + if (!e || !e->message) + git_error_set(e ? e->klass : GIT_ERROR_CALLBACK, + "%s callback returned %d", action, error_code); + } + return error_code; +} + +#ifdef GIT_WIN32 +#define git_error_set_after_callback(code) \ + git_error_set_after_callback_function((code), __FUNCTION__) +#else +#define git_error_set_after_callback(code) \ + git_error_set_after_callback_function((code), __func__) +#endif + +/** + * Gets the system error code for this thread. + */ +int git_error_system_last(void); + +/** + * Sets the system error code for this thread. + */ +void git_error_system_set(int code); + +/** + * Structure to preserve libgit2 error state + */ +typedef struct { + int error_code; + unsigned int oom : 1; + git_error error_msg; +} git_error_state; + +/** + * Capture current error state to restore later, returning error code. + * If `error_code` is zero, this does not clear the current error state. + * You must either restore this error state, or free it. + */ +extern int git_error_state_capture(git_error_state *state, int error_code); + +/** + * Restore error state to a previous value, returning saved error code. + */ +extern int git_error_state_restore(git_error_state *state); + +/** Free an error state. */ +extern void git_error_state_free(git_error_state *state); + +#endif diff --git a/src/libgit2/experimental.h.in b/src/libgit2/experimental.h.in new file mode 100644 index 0000000..25fb14b --- /dev/null +++ b/src/libgit2/experimental.h.in @@ -0,0 +1,13 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_experimental_h__ +#define INCLUDE_experimental_h__ + +#cmakedefine GIT_EXPERIMENTAL_SHA256 1 + +#endif diff --git a/src/libgit2/fetch.c b/src/libgit2/fetch.c new file mode 100644 index 0000000..d74abb4 --- /dev/null +++ b/src/libgit2/fetch.c @@ -0,0 +1,239 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "fetch.h" + +#include "git2/oid.h" +#include "git2/refs.h" +#include "git2/revwalk.h" +#include "git2/transport.h" +#include "git2/sys/remote.h" + +#include "oid.h" +#include "remote.h" +#include "refspec.h" +#include "pack.h" +#include "repository.h" +#include "refs.h" +#include "transports/smart.h" + +static int maybe_want(git_remote *remote, git_remote_head *head, git_refspec *tagspec, git_remote_autotag_option_t tagopt) +{ + int match = 0, valid; + + if (git_reference_name_is_valid(&valid, head->name) < 0) + return -1; + + if (!valid) + return 0; + + if (tagopt == GIT_REMOTE_DOWNLOAD_TAGS_ALL) { + /* + * If tagopt is --tags, always request tags + * in addition to the remote's refspecs + */ + if (git_refspec_src_matches(tagspec, head->name)) + match = 1; + } + + if (!match && git_remote__matching_refspec(remote, head->name)) + match = 1; + + if (!match) + return 0; + + return git_vector_insert(&remote->refs, head); +} + +static int mark_local(git_remote *remote) +{ + git_remote_head *head; + git_odb *odb; + size_t i; + + if (git_repository_odb__weakptr(&odb, remote->repo) < 0) + return -1; + + git_vector_foreach(&remote->refs, i, head) { + /* If we have the object, mark it so we don't ask for it. + However if we are unshallowing, we need to ask for it + even though the head exists locally. */ + if (remote->nego.depth != INT_MAX && git_odb_exists(odb, &head->oid)) + head->local = 1; + else + remote->need_pack = 1; + } + + return 0; +} + +static int maybe_want_oid(git_remote *remote, git_refspec *spec) +{ + git_remote_head *oid_head; + + oid_head = git__calloc(1, sizeof(git_remote_head)); + GIT_ERROR_CHECK_ALLOC(oid_head); + + git_oid__fromstr(&oid_head->oid, spec->src, remote->repo->oid_type); + + if (spec->dst) { + oid_head->name = git__strdup(spec->dst); + GIT_ERROR_CHECK_ALLOC(oid_head->name); + } + + if (git_vector_insert(&remote->local_heads, oid_head) < 0 || + git_vector_insert(&remote->refs, oid_head) < 0) + return -1; + + return 0; +} + +static int filter_wants(git_remote *remote, const git_fetch_options *opts) +{ + git_remote_head **heads; + git_refspec tagspec, head, *spec; + int error = 0; + size_t i, heads_len; + unsigned int remote_caps; + unsigned int oid_mask = GIT_REMOTE_CAPABILITY_TIP_OID | + GIT_REMOTE_CAPABILITY_REACHABLE_OID; + git_remote_autotag_option_t tagopt = remote->download_tags; + + if (opts && opts->download_tags != GIT_REMOTE_DOWNLOAD_TAGS_UNSPECIFIED) + tagopt = opts->download_tags; + + git_vector_clear(&remote->refs); + if ((error = git_refspec__parse(&tagspec, GIT_REFSPEC_TAGS, true)) < 0) + return error; + + /* + * The fetch refspec can be NULL, and what this means is that the + * user didn't specify one. This is fine, as it means that we're + * not interested in any particular branch but just the remote's + * HEAD, which will be stored in FETCH_HEAD after the fetch. + */ + if (remote->active_refspecs.length == 0) { + if ((error = git_refspec__parse(&head, "HEAD", true)) < 0) + goto cleanup; + + error = git_refspec__dwim_one(&remote->active_refspecs, &head, &remote->refs); + git_refspec__dispose(&head); + + if (error < 0) + goto cleanup; + } + + if ((error = git_remote_ls((const git_remote_head ***)&heads, &heads_len, remote)) < 0 || + (error = git_remote_capabilities(&remote_caps, remote)) < 0) + goto cleanup; + + /* Handle remote heads */ + for (i = 0; i < heads_len; i++) { + if ((error = maybe_want(remote, heads[i], &tagspec, tagopt)) < 0) + goto cleanup; + } + + /* Handle explicitly specified OID specs */ + git_vector_foreach(&remote->active_refspecs, i, spec) { + if (!git_oid__is_hexstr(spec->src, remote->repo->oid_type)) + continue; + + if (!(remote_caps & oid_mask)) { + git_error_set(GIT_ERROR_INVALID, "cannot fetch a specific object from the remote repository"); + error = -1; + goto cleanup; + } + + if ((error = maybe_want_oid(remote, spec)) < 0) + goto cleanup; + } + + error = mark_local(remote); + +cleanup: + git_refspec__dispose(&tagspec); + + return error; +} + +/* + * In this first version, we push all our refs in and start sending + * them out. When we get an ACK we hide that commit and continue + * traversing until we're done + */ +int git_fetch_negotiate(git_remote *remote, const git_fetch_options *opts) +{ + git_transport *t = remote->transport; + int error; + + remote->need_pack = 0; + + if (opts) { + GIT_ASSERT_ARG(opts->depth >= 0); + remote->nego.depth = opts->depth; + } + + if (filter_wants(remote, opts) < 0) + return -1; + + /* Don't try to negotiate when we don't want anything */ + if (!remote->need_pack) + return 0; + + /* + * Now we have everything set up so we can start tell the + * server what we want and what we have. + */ + remote->nego.refs = (const git_remote_head * const *)remote->refs.contents; + remote->nego.refs_len = remote->refs.length; + + if (git_repository__shallow_roots(&remote->nego.shallow_roots, + &remote->nego.shallow_roots_len, + remote->repo) < 0) + return -1; + + error = t->negotiate_fetch(t, + remote->repo, + &remote->nego); + + git__free(remote->nego.shallow_roots); + + return error; +} + +int git_fetch_download_pack(git_remote *remote) +{ + git_oidarray shallow_roots = { NULL }; + git_transport *t = remote->transport; + int error; + + if (!remote->need_pack) + return 0; + + if ((error = t->download_pack(t, remote->repo, &remote->stats)) != 0 || + (error = t->shallow_roots(&shallow_roots, t)) != 0) + return error; + + error = git_repository__shallow_roots_write(remote->repo, &shallow_roots); + + git_oidarray_dispose(&shallow_roots); + return error; +} + +int git_fetch_options_init(git_fetch_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_fetch_options, GIT_FETCH_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_fetch_init_options(git_fetch_options *opts, unsigned int version) +{ + return git_fetch_options_init(opts, version); +} +#endif diff --git a/src/libgit2/fetch.h b/src/libgit2/fetch.h new file mode 100644 index 0000000..493366d --- /dev/null +++ b/src/libgit2/fetch.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_fetch_h__ +#define INCLUDE_fetch_h__ + +#include "common.h" + +#include "git2/remote.h" + +int git_fetch_negotiate(git_remote *remote, const git_fetch_options *opts); + +int git_fetch_download_pack(git_remote *remote); + +int git_fetch_setup_walk(git_revwalk **out, git_repository *repo); + +#endif diff --git a/src/libgit2/fetchhead.c b/src/libgit2/fetchhead.c new file mode 100644 index 0000000..2f276e5 --- /dev/null +++ b/src/libgit2/fetchhead.c @@ -0,0 +1,340 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "fetchhead.h" + +#include "git2/types.h" +#include "git2/oid.h" + +#include "str.h" +#include "futils.h" +#include "filebuf.h" +#include "refs.h" +#include "net.h" +#include "repository.h" + +int git_fetchhead_ref_cmp(const void *a, const void *b) +{ + const git_fetchhead_ref *one = (const git_fetchhead_ref *)a; + const git_fetchhead_ref *two = (const git_fetchhead_ref *)b; + + if (one->is_merge && !two->is_merge) + return -1; + if (two->is_merge && !one->is_merge) + return 1; + + if (one->ref_name && two->ref_name) + return strcmp(one->ref_name, two->ref_name); + else if (one->ref_name) + return -1; + else if (two->ref_name) + return 1; + + return 0; +} + +static char *sanitized_remote_url(const char *remote_url) +{ + git_net_url url = GIT_NET_URL_INIT; + char *sanitized = NULL; + int error; + + if (git_net_url_parse(&url, remote_url) == 0) { + git_str buf = GIT_STR_INIT; + + git__free(url.username); + git__free(url.password); + url.username = url.password = NULL; + + if ((error = git_net_url_fmt(&buf, &url)) < 0) + goto fallback; + + sanitized = git_str_detach(&buf); + } + +fallback: + if (!sanitized) + sanitized = git__strdup(remote_url); + + git_net_url_dispose(&url); + return sanitized; +} + +int git_fetchhead_ref_create( + git_fetchhead_ref **out, + git_oid *oid, + unsigned int is_merge, + const char *ref_name, + const char *remote_url) +{ + git_fetchhead_ref *fetchhead_ref; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(oid); + + *out = NULL; + + fetchhead_ref = git__malloc(sizeof(git_fetchhead_ref)); + GIT_ERROR_CHECK_ALLOC(fetchhead_ref); + + memset(fetchhead_ref, 0x0, sizeof(git_fetchhead_ref)); + + git_oid_cpy(&fetchhead_ref->oid, oid); + fetchhead_ref->is_merge = is_merge; + + if (ref_name) { + fetchhead_ref->ref_name = git__strdup(ref_name); + GIT_ERROR_CHECK_ALLOC(fetchhead_ref->ref_name); + } + + if (remote_url) { + fetchhead_ref->remote_url = sanitized_remote_url(remote_url); + GIT_ERROR_CHECK_ALLOC(fetchhead_ref->remote_url); + } + + *out = fetchhead_ref; + + return 0; +} + +static int fetchhead_ref_write( + git_filebuf *file, + git_fetchhead_ref *fetchhead_ref) +{ + char oid[GIT_OID_MAX_HEXSIZE + 1]; + const char *type, *name; + int head = 0; + + GIT_ASSERT_ARG(file); + GIT_ASSERT_ARG(fetchhead_ref); + + git_oid_tostr(oid, GIT_OID_MAX_HEXSIZE + 1, &fetchhead_ref->oid); + + if (git__prefixcmp(fetchhead_ref->ref_name, GIT_REFS_HEADS_DIR) == 0) { + type = "branch "; + name = fetchhead_ref->ref_name + strlen(GIT_REFS_HEADS_DIR); + } else if(git__prefixcmp(fetchhead_ref->ref_name, + GIT_REFS_TAGS_DIR) == 0) { + type = "tag "; + name = fetchhead_ref->ref_name + strlen(GIT_REFS_TAGS_DIR); + } else if (!git__strcmp(fetchhead_ref->ref_name, GIT_HEAD_FILE)) { + head = 1; + } else { + type = ""; + name = fetchhead_ref->ref_name; + } + + if (head) + return git_filebuf_printf(file, "%s\t\t%s\n", oid, fetchhead_ref->remote_url); + + return git_filebuf_printf(file, "%s\t%s\t%s'%s' of %s\n", + oid, + (fetchhead_ref->is_merge) ? "" : "not-for-merge", + type, + name, + fetchhead_ref->remote_url); +} + +int git_fetchhead_write(git_repository *repo, git_vector *fetchhead_refs) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_str path = GIT_STR_INIT; + unsigned int i; + git_fetchhead_ref *fetchhead_ref; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(fetchhead_refs); + + if (git_str_joinpath(&path, repo->gitdir, GIT_FETCH_HEAD_FILE) < 0) + return -1; + + if (git_filebuf_open(&file, path.ptr, GIT_FILEBUF_APPEND, GIT_REFS_FILE_MODE) < 0) { + git_str_dispose(&path); + return -1; + } + + git_str_dispose(&path); + + git_vector_sort(fetchhead_refs); + + git_vector_foreach(fetchhead_refs, i, fetchhead_ref) + fetchhead_ref_write(&file, fetchhead_ref); + + return git_filebuf_commit(&file); +} + +static int fetchhead_ref_parse( + git_oid *oid, + unsigned int *is_merge, + git_str *ref_name, + const char **remote_url, + char *line, + size_t line_num, + git_oid_t oid_type) +{ + char *oid_str, *is_merge_str, *desc, *name = NULL; + const char *type = NULL; + int error = 0; + + *remote_url = NULL; + + if (!*line) { + git_error_set(GIT_ERROR_FETCHHEAD, + "empty line in FETCH_HEAD line %"PRIuZ, line_num); + return -1; + } + + /* Compat with old git clients that wrote FETCH_HEAD like a loose ref. */ + if ((oid_str = git__strsep(&line, "\t")) == NULL) { + oid_str = line; + line += strlen(line); + + *is_merge = 1; + } + + if (strlen(oid_str) != git_oid_hexsize(oid_type)) { + git_error_set(GIT_ERROR_FETCHHEAD, + "invalid object ID in FETCH_HEAD line %"PRIuZ, line_num); + return -1; + } + + if (git_oid__fromstr(oid, oid_str, oid_type) < 0) { + const git_error *oid_err = git_error_last(); + const char *err_msg = oid_err ? oid_err->message : "invalid object ID"; + + git_error_set(GIT_ERROR_FETCHHEAD, "%s in FETCH_HEAD line %"PRIuZ, + err_msg, line_num); + return -1; + } + + /* Parse new data from newer git clients */ + if (*line) { + if ((is_merge_str = git__strsep(&line, "\t")) == NULL) { + git_error_set(GIT_ERROR_FETCHHEAD, + "invalid description data in FETCH_HEAD line %"PRIuZ, line_num); + return -1; + } + + if (*is_merge_str == '\0') + *is_merge = 1; + else if (strcmp(is_merge_str, "not-for-merge") == 0) + *is_merge = 0; + else { + git_error_set(GIT_ERROR_FETCHHEAD, + "invalid for-merge entry in FETCH_HEAD line %"PRIuZ, line_num); + return -1; + } + + if ((desc = line) == NULL) { + git_error_set(GIT_ERROR_FETCHHEAD, + "invalid description in FETCH_HEAD line %"PRIuZ, line_num); + return -1; + } + + if (git__prefixcmp(desc, "branch '") == 0) { + type = GIT_REFS_HEADS_DIR; + name = desc + 8; + } else if (git__prefixcmp(desc, "tag '") == 0) { + type = GIT_REFS_TAGS_DIR; + name = desc + 5; + } else if (git__prefixcmp(desc, "'") == 0) + name = desc + 1; + + if (name) { + if ((desc = strstr(name, "' ")) == NULL || + git__prefixcmp(desc, "' of ") != 0) { + git_error_set(GIT_ERROR_FETCHHEAD, + "invalid description in FETCH_HEAD line %"PRIuZ, line_num); + return -1; + } + + *desc = '\0'; + desc += 5; + } + + *remote_url = desc; + } + + git_str_clear(ref_name); + + if (type) + git_str_join(ref_name, '/', type, name); + else if(name) + git_str_puts(ref_name, name); + + return error; +} + +int git_repository_fetchhead_foreach( + git_repository *repo, + git_repository_fetchhead_foreach_cb cb, + void *payload) +{ + git_str path = GIT_STR_INIT, file = GIT_STR_INIT, name = GIT_STR_INIT; + const char *ref_name; + git_oid oid; + const char *remote_url; + unsigned int is_merge = 0; + char *buffer, *line; + size_t line_num = 0; + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(cb); + + if (git_str_joinpath(&path, repo->gitdir, GIT_FETCH_HEAD_FILE) < 0) + return -1; + + if ((error = git_futils_readbuffer(&file, git_str_cstr(&path))) < 0) + goto done; + + buffer = file.ptr; + + while ((line = git__strsep(&buffer, "\n")) != NULL) { + ++line_num; + + if ((error = fetchhead_ref_parse(&oid, &is_merge, &name, + &remote_url, line, line_num, + repo->oid_type)) < 0) + goto done; + + if (git_str_len(&name) > 0) + ref_name = git_str_cstr(&name); + else + ref_name = NULL; + + error = cb(ref_name, remote_url, &oid, is_merge, payload); + if (error) { + git_error_set_after_callback(error); + goto done; + } + } + + if (*buffer) { + git_error_set(GIT_ERROR_FETCHHEAD, "no EOL at line %"PRIuZ, line_num+1); + error = -1; + goto done; + } + +done: + git_str_dispose(&file); + git_str_dispose(&path); + git_str_dispose(&name); + + return error; +} + +void git_fetchhead_ref_free(git_fetchhead_ref *fetchhead_ref) +{ + if (fetchhead_ref == NULL) + return; + + git__free(fetchhead_ref->remote_url); + git__free(fetchhead_ref->ref_name); + git__free(fetchhead_ref); +} + diff --git a/src/libgit2/fetchhead.h b/src/libgit2/fetchhead.h new file mode 100644 index 0000000..9e51710 --- /dev/null +++ b/src/libgit2/fetchhead.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_fetchhead_h__ +#define INCLUDE_fetchhead_h__ + +#include "common.h" + +#include "oid.h" +#include "vector.h" + +typedef struct git_fetchhead_ref { + git_oid oid; + unsigned int is_merge; + char *ref_name; + char *remote_url; +} git_fetchhead_ref; + +int git_fetchhead_ref_create( + git_fetchhead_ref **fetchhead_ref_out, + git_oid *oid, + unsigned int is_merge, + const char *ref_name, + const char *remote_url); + +int git_fetchhead_ref_cmp(const void *a, const void *b); + +int git_fetchhead_write(git_repository *repo, git_vector *fetchhead_refs); + +void git_fetchhead_ref_free(git_fetchhead_ref *fetchhead_ref); + +#endif diff --git a/src/libgit2/filter.c b/src/libgit2/filter.c new file mode 100644 index 0000000..80a3cae --- /dev/null +++ b/src/libgit2/filter.c @@ -0,0 +1,1221 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "filter.h" + +#include "buf.h" +#include "common.h" +#include "futils.h" +#include "hash.h" +#include "repository.h" +#include "runtime.h" +#include "git2/sys/filter.h" +#include "git2/config.h" +#include "blob.h" +#include "attr_file.h" +#include "array.h" +#include "path.h" + +struct git_filter_source { + git_repository *repo; + const char *path; + git_oid oid; /* zero if unknown (which is likely) */ + uint16_t filemode; /* zero if unknown */ + git_filter_mode_t mode; + git_filter_options options; +}; + +typedef struct { + const char *filter_name; + git_filter *filter; + void *payload; +} git_filter_entry; + +struct git_filter_list { + git_array_t(git_filter_entry) filters; + git_filter_source source; + git_str *temp_buf; + char path[GIT_FLEX_ARRAY]; +}; + +typedef struct { + char *filter_name; + git_filter *filter; + int priority; + int initialized; + size_t nattrs, nmatches; + char *attrdata; + const char *attrs[GIT_FLEX_ARRAY]; +} git_filter_def; + +static int filter_def_priority_cmp(const void *a, const void *b) +{ + int pa = ((const git_filter_def *)a)->priority; + int pb = ((const git_filter_def *)b)->priority; + return (pa < pb) ? -1 : (pa > pb) ? 1 : 0; +} + +struct git_filter_registry { + git_rwlock lock; + git_vector filters; +}; + +static struct git_filter_registry filter_registry; + +static void git_filter_global_shutdown(void); + + +static int filter_def_scan_attrs( + git_str *attrs, size_t *nattr, size_t *nmatch, const char *attr_str) +{ + const char *start, *scan = attr_str; + int has_eq; + + *nattr = *nmatch = 0; + + if (!scan) + return 0; + + while (*scan) { + while (git__isspace(*scan)) scan++; + + for (start = scan, has_eq = 0; *scan && !git__isspace(*scan); ++scan) { + if (*scan == '=') + has_eq = 1; + } + + if (scan > start) { + (*nattr)++; + if (has_eq || *start == '-' || *start == '+' || *start == '!') + (*nmatch)++; + + if (has_eq) + git_str_putc(attrs, '='); + git_str_put(attrs, start, scan - start); + git_str_putc(attrs, '\0'); + } + } + + return 0; +} + +static void filter_def_set_attrs(git_filter_def *fdef) +{ + char *scan = fdef->attrdata; + size_t i; + + for (i = 0; i < fdef->nattrs; ++i) { + const char *name, *value; + + switch (*scan) { + case '=': + name = scan + 1; + for (scan++; *scan != '='; scan++) /* find '=' */; + *scan++ = '\0'; + value = scan; + break; + case '-': + name = scan + 1; value = git_attr__false; break; + case '+': + name = scan + 1; value = git_attr__true; break; + case '!': + name = scan + 1; value = git_attr__unset; break; + default: + name = scan; value = NULL; break; + } + + fdef->attrs[i] = name; + fdef->attrs[i + fdef->nattrs] = value; + + scan += strlen(scan) + 1; + } +} + +static int filter_def_name_key_check(const void *key, const void *fdef) +{ + const char *name = + fdef ? ((const git_filter_def *)fdef)->filter_name : NULL; + return name ? git__strcmp(key, name) : -1; +} + +static int filter_def_filter_key_check(const void *key, const void *fdef) +{ + const void *filter = fdef ? ((const git_filter_def *)fdef)->filter : NULL; + return (key == filter) ? 0 : -1; +} + +/* Note: callers must lock the registry before calling this function */ +static int filter_registry_insert( + const char *name, git_filter *filter, int priority) +{ + git_filter_def *fdef; + size_t nattr = 0, nmatch = 0, alloc_len; + git_str attrs = GIT_STR_INIT; + + if (filter_def_scan_attrs(&attrs, &nattr, &nmatch, filter->attributes) < 0) + return -1; + + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&alloc_len, nattr, 2); + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&alloc_len, alloc_len, sizeof(char *)); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, sizeof(git_filter_def)); + + fdef = git__calloc(1, alloc_len); + GIT_ERROR_CHECK_ALLOC(fdef); + + fdef->filter_name = git__strdup(name); + GIT_ERROR_CHECK_ALLOC(fdef->filter_name); + + fdef->filter = filter; + fdef->priority = priority; + fdef->nattrs = nattr; + fdef->nmatches = nmatch; + fdef->attrdata = git_str_detach(&attrs); + + filter_def_set_attrs(fdef); + + if (git_vector_insert(&filter_registry.filters, fdef) < 0) { + git__free(fdef->filter_name); + git__free(fdef->attrdata); + git__free(fdef); + return -1; + } + + git_vector_sort(&filter_registry.filters); + return 0; +} + +int git_filter_global_init(void) +{ + git_filter *crlf = NULL, *ident = NULL; + int error = 0; + + if (git_rwlock_init(&filter_registry.lock) < 0) + return -1; + + if ((error = git_vector_init(&filter_registry.filters, 2, + filter_def_priority_cmp)) < 0) + goto done; + + if ((crlf = git_crlf_filter_new()) == NULL || + filter_registry_insert( + GIT_FILTER_CRLF, crlf, GIT_FILTER_CRLF_PRIORITY) < 0 || + (ident = git_ident_filter_new()) == NULL || + filter_registry_insert( + GIT_FILTER_IDENT, ident, GIT_FILTER_IDENT_PRIORITY) < 0) + error = -1; + + if (!error) + error = git_runtime_shutdown_register(git_filter_global_shutdown); + +done: + if (error) { + git_filter_free(crlf); + git_filter_free(ident); + } + + return error; +} + +static void git_filter_global_shutdown(void) +{ + size_t pos; + git_filter_def *fdef; + + if (git_rwlock_wrlock(&filter_registry.lock) < 0) + return; + + git_vector_foreach(&filter_registry.filters, pos, fdef) { + if (fdef->filter && fdef->filter->shutdown) { + fdef->filter->shutdown(fdef->filter); + fdef->initialized = false; + } + + git__free(fdef->filter_name); + git__free(fdef->attrdata); + git__free(fdef); + } + + git_vector_free(&filter_registry.filters); + + git_rwlock_wrunlock(&filter_registry.lock); + git_rwlock_free(&filter_registry.lock); +} + +/* Note: callers must lock the registry before calling this function */ +static int filter_registry_find(size_t *pos, const char *name) +{ + return git_vector_search2( + pos, &filter_registry.filters, filter_def_name_key_check, name); +} + +/* Note: callers must lock the registry before calling this function */ +static git_filter_def *filter_registry_lookup(size_t *pos, const char *name) +{ + git_filter_def *fdef = NULL; + + if (!filter_registry_find(pos, name)) + fdef = git_vector_get(&filter_registry.filters, *pos); + + return fdef; +} + + +int git_filter_register( + const char *name, git_filter *filter, int priority) +{ + int error; + + GIT_ASSERT_ARG(name); + GIT_ASSERT_ARG(filter); + + if (git_rwlock_wrlock(&filter_registry.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock filter registry"); + return -1; + } + + if (!filter_registry_find(NULL, name)) { + git_error_set( + GIT_ERROR_FILTER, "attempt to reregister existing filter '%s'", name); + error = GIT_EEXISTS; + goto done; + } + + error = filter_registry_insert(name, filter, priority); + +done: + git_rwlock_wrunlock(&filter_registry.lock); + return error; +} + +int git_filter_unregister(const char *name) +{ + size_t pos; + git_filter_def *fdef; + int error = 0; + + GIT_ASSERT_ARG(name); + + /* cannot unregister default filters */ + if (!strcmp(GIT_FILTER_CRLF, name) || !strcmp(GIT_FILTER_IDENT, name)) { + git_error_set(GIT_ERROR_FILTER, "cannot unregister filter '%s'", name); + return -1; + } + + if (git_rwlock_wrlock(&filter_registry.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock filter registry"); + return -1; + } + + if ((fdef = filter_registry_lookup(&pos, name)) == NULL) { + git_error_set(GIT_ERROR_FILTER, "cannot find filter '%s' to unregister", name); + error = GIT_ENOTFOUND; + goto done; + } + + git_vector_remove(&filter_registry.filters, pos); + + if (fdef->initialized && fdef->filter && fdef->filter->shutdown) { + fdef->filter->shutdown(fdef->filter); + fdef->initialized = false; + } + + git__free(fdef->filter_name); + git__free(fdef->attrdata); + git__free(fdef); + +done: + git_rwlock_wrunlock(&filter_registry.lock); + return error; +} + +static int filter_initialize(git_filter_def *fdef) +{ + int error = 0; + + if (!fdef->initialized && fdef->filter && fdef->filter->initialize) { + if ((error = fdef->filter->initialize(fdef->filter)) < 0) + return error; + } + + fdef->initialized = true; + return 0; +} + +git_filter *git_filter_lookup(const char *name) +{ + size_t pos; + git_filter_def *fdef; + git_filter *filter = NULL; + + if (git_rwlock_rdlock(&filter_registry.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock filter registry"); + return NULL; + } + + if ((fdef = filter_registry_lookup(&pos, name)) == NULL || + (!fdef->initialized && filter_initialize(fdef) < 0)) + goto done; + + filter = fdef->filter; + +done: + git_rwlock_rdunlock(&filter_registry.lock); + return filter; +} + +void git_filter_free(git_filter *filter) +{ + git__free(filter); +} + +git_repository *git_filter_source_repo(const git_filter_source *src) +{ + return src->repo; +} + +const char *git_filter_source_path(const git_filter_source *src) +{ + return src->path; +} + +uint16_t git_filter_source_filemode(const git_filter_source *src) +{ + return src->filemode; +} + +const git_oid *git_filter_source_id(const git_filter_source *src) +{ + return git_oid_is_zero(&src->oid) ? NULL : &src->oid; +} + +git_filter_mode_t git_filter_source_mode(const git_filter_source *src) +{ + return src->mode; +} + +uint32_t git_filter_source_flags(const git_filter_source *src) +{ + return src->options.flags; +} + +static int filter_list_new( + git_filter_list **out, const git_filter_source *src) +{ + git_filter_list *fl = NULL; + size_t pathlen = src->path ? strlen(src->path) : 0, alloclen; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, sizeof(git_filter_list), pathlen); + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, 1); + + fl = git__calloc(1, alloclen); + GIT_ERROR_CHECK_ALLOC(fl); + + if (src->path) + memcpy(fl->path, src->path, pathlen); + fl->source.repo = src->repo; + fl->source.path = fl->path; + fl->source.mode = src->mode; + + memcpy(&fl->source.options, &src->options, sizeof(git_filter_options)); + + *out = fl; + return 0; +} + +static int filter_list_check_attributes( + const char ***out, + git_repository *repo, + git_filter_session *filter_session, + git_filter_def *fdef, + const git_filter_source *src) +{ + const char **strs = git__calloc(fdef->nattrs, sizeof(const char *)); + git_attr_options attr_opts = GIT_ATTR_OPTIONS_INIT; + size_t i; + int error; + + GIT_ERROR_CHECK_ALLOC(strs); + + if ((src->options.flags & GIT_FILTER_NO_SYSTEM_ATTRIBUTES) != 0) + attr_opts.flags |= GIT_ATTR_CHECK_NO_SYSTEM; + + if ((src->options.flags & GIT_FILTER_ATTRIBUTES_FROM_HEAD) != 0) + attr_opts.flags |= GIT_ATTR_CHECK_INCLUDE_HEAD; + + if ((src->options.flags & GIT_FILTER_ATTRIBUTES_FROM_COMMIT) != 0) { + attr_opts.flags |= GIT_ATTR_CHECK_INCLUDE_COMMIT; + +#ifndef GIT_DEPRECATE_HARD + if (src->options.commit_id) + git_oid_cpy(&attr_opts.attr_commit_id, src->options.commit_id); + else +#endif + git_oid_cpy(&attr_opts.attr_commit_id, &src->options.attr_commit_id); + } + + error = git_attr_get_many_with_session( + strs, repo, filter_session->attr_session, &attr_opts, src->path, fdef->nattrs, fdef->attrs); + + /* if no values were found but no matches are needed, it's okay! */ + if (error == GIT_ENOTFOUND && !fdef->nmatches) { + git_error_clear(); + git__free((void *)strs); + return 0; + } + + for (i = 0; !error && i < fdef->nattrs; ++i) { + const char *want = fdef->attrs[fdef->nattrs + i]; + git_attr_value_t want_type, found_type; + + if (!want) + continue; + + want_type = git_attr_value(want); + found_type = git_attr_value(strs[i]); + + if (want_type != found_type) + error = GIT_ENOTFOUND; + else if (want_type == GIT_ATTR_VALUE_STRING && + strcmp(want, strs[i]) && + strcmp(want, "*")) + error = GIT_ENOTFOUND; + } + + if (error) + git__free((void *)strs); + else + *out = strs; + + return error; +} + +int git_filter_list_new( + git_filter_list **out, + git_repository *repo, + git_filter_mode_t mode, + uint32_t flags) +{ + git_filter_source src = { 0 }; + src.repo = repo; + src.path = NULL; + src.mode = mode; + src.options.flags = flags; + return filter_list_new(out, &src); +} + +int git_filter_list__load( + git_filter_list **filters, + git_repository *repo, + git_blob *blob, /* can be NULL */ + const char *path, + git_filter_mode_t mode, + git_filter_session *filter_session) +{ + int error = 0; + git_filter_list *fl = NULL; + git_filter_source src = { 0 }; + git_filter_entry *fe; + size_t idx; + git_filter_def *fdef; + + if (git_rwlock_rdlock(&filter_registry.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock filter registry"); + return -1; + } + + src.repo = repo; + src.path = path; + src.mode = mode; + + memcpy(&src.options, &filter_session->options, sizeof(git_filter_options)); + + if (blob) + git_oid_cpy(&src.oid, git_blob_id(blob)); + + git_vector_foreach(&filter_registry.filters, idx, fdef) { + const char **values = NULL; + void *payload = NULL; + + if (!fdef || !fdef->filter) + continue; + + if (fdef->nattrs > 0) { + error = filter_list_check_attributes( + &values, repo, + filter_session, fdef, &src); + + if (error == GIT_ENOTFOUND) { + error = 0; + continue; + } else if (error < 0) + break; + } + + if (!fdef->initialized && (error = filter_initialize(fdef)) < 0) + break; + + if (fdef->filter->check) + error = fdef->filter->check( + fdef->filter, &payload, &src, values); + + git__free((void *)values); + + if (error == GIT_PASSTHROUGH) + error = 0; + else if (error < 0) + break; + else { + if (!fl) { + if ((error = filter_list_new(&fl, &src)) < 0) + break; + + fl->temp_buf = filter_session->temp_buf; + } + + fe = git_array_alloc(fl->filters); + GIT_ERROR_CHECK_ALLOC(fe); + + fe->filter = fdef->filter; + fe->filter_name = fdef->filter_name; + fe->payload = payload; + } + } + + git_rwlock_rdunlock(&filter_registry.lock); + + if (error && fl != NULL) { + git_array_clear(fl->filters); + git__free(fl); + fl = NULL; + } + + *filters = fl; + return error; +} + +int git_filter_list_load_ext( + git_filter_list **filters, + git_repository *repo, + git_blob *blob, /* can be NULL */ + const char *path, + git_filter_mode_t mode, + git_filter_options *opts) +{ + git_filter_session filter_session = GIT_FILTER_SESSION_INIT; + + if (opts) + memcpy(&filter_session.options, opts, sizeof(git_filter_options)); + + return git_filter_list__load( + filters, repo, blob, path, mode, &filter_session); +} + +int git_filter_list_load( + git_filter_list **filters, + git_repository *repo, + git_blob *blob, /* can be NULL */ + const char *path, + git_filter_mode_t mode, + uint32_t flags) +{ + git_filter_session filter_session = GIT_FILTER_SESSION_INIT; + + filter_session.options.flags = flags; + + return git_filter_list__load( + filters, repo, blob, path, mode, &filter_session); +} + +void git_filter_list_free(git_filter_list *fl) +{ + uint32_t i; + + if (!fl) + return; + + for (i = 0; i < git_array_size(fl->filters); ++i) { + git_filter_entry *fe = git_array_get(fl->filters, i); + if (fe->filter->cleanup) + fe->filter->cleanup(fe->filter, fe->payload); + } + + git_array_clear(fl->filters); + git__free(fl); +} + +int git_filter_list_contains( + git_filter_list *fl, + const char *name) +{ + size_t i; + + GIT_ASSERT_ARG(name); + + if (!fl) + return 0; + + for (i = 0; i < fl->filters.size; i++) { + if (strcmp(fl->filters.ptr[i].filter_name, name) == 0) + return 1; + } + + return 0; +} + +int git_filter_list_push( + git_filter_list *fl, git_filter *filter, void *payload) +{ + int error = 0; + size_t pos; + git_filter_def *fdef = NULL; + git_filter_entry *fe; + + GIT_ASSERT_ARG(fl); + GIT_ASSERT_ARG(filter); + + if (git_rwlock_rdlock(&filter_registry.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock filter registry"); + return -1; + } + + if (git_vector_search2( + &pos, &filter_registry.filters, + filter_def_filter_key_check, filter) == 0) + fdef = git_vector_get(&filter_registry.filters, pos); + + git_rwlock_rdunlock(&filter_registry.lock); + + if (fdef == NULL) { + git_error_set(GIT_ERROR_FILTER, "cannot use an unregistered filter"); + return -1; + } + + if (!fdef->initialized && (error = filter_initialize(fdef)) < 0) + return error; + + fe = git_array_alloc(fl->filters); + GIT_ERROR_CHECK_ALLOC(fe); + fe->filter = filter; + fe->payload = payload; + + return 0; +} + +size_t git_filter_list_length(const git_filter_list *fl) +{ + return fl ? git_array_size(fl->filters) : 0; +} + +struct buf_stream { + git_writestream parent; + git_str *target; + bool complete; +}; + +static int buf_stream_write( + git_writestream *s, const char *buffer, size_t len) +{ + struct buf_stream *buf_stream = (struct buf_stream *)s; + GIT_ASSERT_ARG(buf_stream); + GIT_ASSERT(buf_stream->complete == 0); + + return git_str_put(buf_stream->target, buffer, len); +} + +static int buf_stream_close(git_writestream *s) +{ + struct buf_stream *buf_stream = (struct buf_stream *)s; + GIT_ASSERT_ARG(buf_stream); + + GIT_ASSERT(buf_stream->complete == 0); + buf_stream->complete = 1; + + return 0; +} + +static void buf_stream_free(git_writestream *s) +{ + GIT_UNUSED(s); +} + +static void buf_stream_init(struct buf_stream *writer, git_str *target) +{ + memset(writer, 0, sizeof(struct buf_stream)); + + writer->parent.write = buf_stream_write; + writer->parent.close = buf_stream_close; + writer->parent.free = buf_stream_free; + writer->target = target; + + git_str_clear(target); +} + +int git_filter_list_apply_to_buffer( + git_buf *out, + git_filter_list *filters, + const char *in, + size_t in_len) +{ + GIT_BUF_WRAP_PRIVATE(out, git_filter_list__apply_to_buffer, filters, in, in_len); +} + +int git_filter_list__apply_to_buffer( + git_str *out, + git_filter_list *filters, + const char *in, + size_t in_len) +{ + struct buf_stream writer; + int error; + + buf_stream_init(&writer, out); + + if ((error = git_filter_list_stream_buffer(filters, + in, in_len, &writer.parent)) < 0) + return error; + + GIT_ASSERT(writer.complete); + return error; +} + +int git_filter_list__convert_buf( + git_str *out, + git_filter_list *filters, + git_str *in) +{ + int error; + + if (!filters || git_filter_list_length(filters) == 0) { + git_str_swap(out, in); + git_str_dispose(in); + return 0; + } + + error = git_filter_list__apply_to_buffer(out, filters, + in->ptr, in->size); + + if (!error) + git_str_dispose(in); + + return error; +} + +int git_filter_list_apply_to_file( + git_buf *out, + git_filter_list *filters, + git_repository *repo, + const char *path) +{ + GIT_BUF_WRAP_PRIVATE(out, git_filter_list__apply_to_file, filters, repo, path); +} + +int git_filter_list__apply_to_file( + git_str *out, + git_filter_list *filters, + git_repository *repo, + const char *path) +{ + struct buf_stream writer; + int error; + + buf_stream_init(&writer, out); + + if ((error = git_filter_list_stream_file( + filters, repo, path, &writer.parent)) < 0) + return error; + + GIT_ASSERT(writer.complete); + return error; +} + +static int buf_from_blob(git_str *out, git_blob *blob) +{ + git_object_size_t rawsize = git_blob_rawsize(blob); + + if (!git__is_sizet(rawsize)) { + git_error_set(GIT_ERROR_OS, "blob is too large to filter"); + return -1; + } + + git_str_attach_notowned(out, git_blob_rawcontent(blob), (size_t)rawsize); + return 0; +} + +int git_filter_list_apply_to_blob( + git_buf *out, + git_filter_list *filters, + git_blob *blob) +{ + GIT_BUF_WRAP_PRIVATE(out, git_filter_list__apply_to_blob, filters, blob); +} + +int git_filter_list__apply_to_blob( + git_str *out, + git_filter_list *filters, + git_blob *blob) +{ + struct buf_stream writer; + int error; + + buf_stream_init(&writer, out); + + if ((error = git_filter_list_stream_blob( + filters, blob, &writer.parent)) < 0) + return error; + + GIT_ASSERT(writer.complete); + return error; +} + +struct buffered_stream { + git_writestream parent; + git_filter *filter; + int (*write_fn)(git_filter *, void **, git_str *, const git_str *, const git_filter_source *); + int (*legacy_write_fn)(git_filter *, void **, git_buf *, const git_buf *, const git_filter_source *); + const git_filter_source *source; + void **payload; + git_str input; + git_str temp_buf; + git_str *output; + git_writestream *target; +}; + +static int buffered_stream_write( + git_writestream *s, const char *buffer, size_t len) +{ + struct buffered_stream *buffered_stream = (struct buffered_stream *)s; + GIT_ASSERT_ARG(buffered_stream); + + return git_str_put(&buffered_stream->input, buffer, len); +} + +#ifndef GIT_DEPRECATE_HARD +# define BUF_TO_STRUCT(b, s) \ + (b)->ptr = (s)->ptr; \ + (b)->size = (s)->size; \ + (b)->reserved = (s)->asize; +# define STRUCT_TO_BUF(s, b) \ + (s)->ptr = (b)->ptr; \ + (s)->size = (b)->size; \ + (s)->asize = (b)->reserved; +#endif + +static int buffered_stream_close(git_writestream *s) +{ + struct buffered_stream *buffered_stream = (struct buffered_stream *)s; + git_str *writebuf; + git_error_state error_state = {0}; + int error; + + GIT_ASSERT_ARG(buffered_stream); + +#ifndef GIT_DEPRECATE_HARD + if (buffered_stream->write_fn == NULL) { + git_buf legacy_output = GIT_BUF_INIT, + legacy_input = GIT_BUF_INIT; + + BUF_TO_STRUCT(&legacy_output, buffered_stream->output); + BUF_TO_STRUCT(&legacy_input, &buffered_stream->input); + + error = buffered_stream->legacy_write_fn( + buffered_stream->filter, + buffered_stream->payload, + &legacy_output, + &legacy_input, + buffered_stream->source); + + STRUCT_TO_BUF(buffered_stream->output, &legacy_output); + STRUCT_TO_BUF(&buffered_stream->input, &legacy_input); + } else +#endif + error = buffered_stream->write_fn( + buffered_stream->filter, + buffered_stream->payload, + buffered_stream->output, + &buffered_stream->input, + buffered_stream->source); + + if (error == GIT_PASSTHROUGH) { + writebuf = &buffered_stream->input; + } else if (error == 0) { + writebuf = buffered_stream->output; + } else { + /* close stream before erroring out taking care + * to preserve the original error */ + git_error_state_capture(&error_state, error); + buffered_stream->target->close(buffered_stream->target); + git_error_state_restore(&error_state); + return error; + } + + if ((error = buffered_stream->target->write( + buffered_stream->target, writebuf->ptr, writebuf->size)) == 0) + error = buffered_stream->target->close(buffered_stream->target); + + return error; +} + +static void buffered_stream_free(git_writestream *s) +{ + struct buffered_stream *buffered_stream = (struct buffered_stream *)s; + + if (buffered_stream) { + git_str_dispose(&buffered_stream->input); + git_str_dispose(&buffered_stream->temp_buf); + git__free(buffered_stream); + } +} + +int git_filter_buffered_stream_new( + git_writestream **out, + git_filter *filter, + int (*write_fn)(git_filter *, void **, git_str *, const git_str *, const git_filter_source *), + git_str *temp_buf, + void **payload, + const git_filter_source *source, + git_writestream *target) +{ + struct buffered_stream *buffered_stream = git__calloc(1, sizeof(struct buffered_stream)); + GIT_ERROR_CHECK_ALLOC(buffered_stream); + + buffered_stream->parent.write = buffered_stream_write; + buffered_stream->parent.close = buffered_stream_close; + buffered_stream->parent.free = buffered_stream_free; + buffered_stream->filter = filter; + buffered_stream->write_fn = write_fn; + buffered_stream->output = temp_buf ? temp_buf : &buffered_stream->temp_buf; + buffered_stream->payload = payload; + buffered_stream->source = source; + buffered_stream->target = target; + + if (temp_buf) + git_str_clear(temp_buf); + + *out = (git_writestream *)buffered_stream; + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +static int buffered_legacy_stream_new( + git_writestream **out, + git_filter *filter, + int (*legacy_write_fn)(git_filter *, void **, git_buf *, const git_buf *, const git_filter_source *), + git_str *temp_buf, + void **payload, + const git_filter_source *source, + git_writestream *target) +{ + struct buffered_stream *buffered_stream = git__calloc(1, sizeof(struct buffered_stream)); + GIT_ERROR_CHECK_ALLOC(buffered_stream); + + buffered_stream->parent.write = buffered_stream_write; + buffered_stream->parent.close = buffered_stream_close; + buffered_stream->parent.free = buffered_stream_free; + buffered_stream->filter = filter; + buffered_stream->legacy_write_fn = legacy_write_fn; + buffered_stream->output = temp_buf ? temp_buf : &buffered_stream->temp_buf; + buffered_stream->payload = payload; + buffered_stream->source = source; + buffered_stream->target = target; + + if (temp_buf) + git_str_clear(temp_buf); + + *out = (git_writestream *)buffered_stream; + return 0; +} +#endif + +static int setup_stream( + git_writestream **out, + git_filter_entry *fe, + git_filter_list *filters, + git_writestream *last_stream) +{ +#ifndef GIT_DEPRECATE_HARD + GIT_ASSERT(fe->filter->stream || fe->filter->apply); + + /* + * If necessary, create a stream that proxies the traditional + * application. + */ + if (!fe->filter->stream) { + /* Create a stream that proxies the one-shot apply */ + return buffered_legacy_stream_new(out, + fe->filter, fe->filter->apply, filters->temp_buf, + &fe->payload, &filters->source, last_stream); + } +#endif + + GIT_ASSERT(fe->filter->stream); + return fe->filter->stream(out, fe->filter, + &fe->payload, &filters->source, last_stream); +} + +static int stream_list_init( + git_writestream **out, + git_vector *streams, + git_filter_list *filters, + git_writestream *target) +{ + git_writestream *last_stream = target; + size_t i; + int error = 0; + + *out = NULL; + + if (!filters) { + *out = target; + return 0; + } + + /* Create filters last to first to get the chaining direction */ + for (i = 0; i < git_array_size(filters->filters); ++i) { + size_t filter_idx = (filters->source.mode == GIT_FILTER_TO_WORKTREE) ? + git_array_size(filters->filters) - 1 - i : i; + + git_filter_entry *fe = git_array_get(filters->filters, filter_idx); + git_writestream *filter_stream; + + error = setup_stream(&filter_stream, fe, filters, last_stream); + + if (error < 0) + goto out; + + git_vector_insert(streams, filter_stream); + last_stream = filter_stream; + } + +out: + if (error) + last_stream->close(last_stream); + else + *out = last_stream; + + return error; +} + +static void filter_streams_free(git_vector *streams) +{ + git_writestream *stream; + size_t i; + + git_vector_foreach(streams, i, stream) + stream->free(stream); + git_vector_free(streams); +} + +int git_filter_list_stream_file( + git_filter_list *filters, + git_repository *repo, + const char *path, + git_writestream *target) +{ + char buf[GIT_BUFSIZE_FILTERIO]; + git_str abspath = GIT_STR_INIT; + const char *base = repo ? git_repository_workdir(repo) : NULL; + git_vector filter_streams = GIT_VECTOR_INIT; + git_writestream *stream_start; + ssize_t readlen; + int fd = -1, error, initialized = 0; + + if ((error = stream_list_init( + &stream_start, &filter_streams, filters, target)) < 0 || + (error = git_fs_path_join_unrooted(&abspath, path, base, NULL)) < 0 || + (error = git_path_validate_str_length(repo, &abspath)) < 0) + goto done; + + initialized = 1; + + if ((fd = git_futils_open_ro(abspath.ptr)) < 0) { + error = fd; + goto done; + } + + while ((readlen = p_read(fd, buf, sizeof(buf))) > 0) { + if ((error = stream_start->write(stream_start, buf, readlen)) < 0) + goto done; + } + + if (readlen < 0) + error = -1; + +done: + if (initialized) + error |= stream_start->close(stream_start); + + if (fd >= 0) + p_close(fd); + filter_streams_free(&filter_streams); + git_str_dispose(&abspath); + return error; +} + +int git_filter_list_stream_buffer( + git_filter_list *filters, + const char *buffer, + size_t len, + git_writestream *target) +{ + git_vector filter_streams = GIT_VECTOR_INIT; + git_writestream *stream_start; + int error, initialized = 0; + + if ((error = stream_list_init(&stream_start, &filter_streams, filters, target)) < 0) + goto out; + initialized = 1; + + if ((error = stream_start->write(stream_start, buffer, len)) < 0) + goto out; + +out: + if (initialized) + error |= stream_start->close(stream_start); + + filter_streams_free(&filter_streams); + return error; +} + +int git_filter_list_stream_blob( + git_filter_list *filters, + git_blob *blob, + git_writestream *target) +{ + git_str in = GIT_STR_INIT; + + if (buf_from_blob(&in, blob) < 0) + return -1; + + if (filters) + git_oid_cpy(&filters->source.oid, git_blob_id(blob)); + + return git_filter_list_stream_buffer(filters, in.ptr, in.size, target); +} + +int git_filter_init(git_filter *filter, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE(filter, version, git_filter, GIT_FILTER_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD + +int git_filter_list_stream_data( + git_filter_list *filters, + git_buf *data, + git_writestream *target) +{ + return git_filter_list_stream_buffer(filters, data->ptr, data->size, target); +} + +int git_filter_list_apply_to_data( + git_buf *tgt, git_filter_list *filters, git_buf *src) +{ + return git_filter_list_apply_to_buffer(tgt, filters, src->ptr, src->size); +} + +#endif diff --git a/src/libgit2/filter.h b/src/libgit2/filter.h new file mode 100644 index 0000000..58cb4b4 --- /dev/null +++ b/src/libgit2/filter.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_filter_h__ +#define INCLUDE_filter_h__ + +#include "common.h" + +#include "attr_file.h" +#include "git2/filter.h" +#include "git2/sys/filter.h" + +/* Amount of file to examine for NUL byte when checking binary-ness */ +#define GIT_FILTER_BYTES_TO_CHECK_NUL 8000 + +typedef struct { + git_filter_options options; + git_attr_session *attr_session; + git_str *temp_buf; +} git_filter_session; + +#define GIT_FILTER_SESSION_INIT {GIT_FILTER_OPTIONS_INIT, 0} + +extern int git_filter_global_init(void); + +extern void git_filter_free(git_filter *filter); + +extern int git_filter_list__load( + git_filter_list **filters, + git_repository *repo, + git_blob *blob, /* can be NULL */ + const char *path, + git_filter_mode_t mode, + git_filter_session *filter_session); + +int git_filter_list__apply_to_buffer( + git_str *out, + git_filter_list *filters, + const char *in, + size_t in_len); +int git_filter_list__apply_to_file( + git_str *out, + git_filter_list *filters, + git_repository *repo, + const char *path); +int git_filter_list__apply_to_blob( + git_str *out, + git_filter_list *filters, + git_blob *blob); + +/* + * The given input buffer will be converted to the given output buffer. + * The input buffer will be freed (_if_ it was allocated). + */ +extern int git_filter_list__convert_buf( + git_str *out, + git_filter_list *filters, + git_str *in); + +extern int git_filter_list__apply_to_file( + git_str *out, + git_filter_list *filters, + git_repository *repo, + const char *path); + +/* + * Available filters + */ + +extern git_filter *git_crlf_filter_new(void); +extern git_filter *git_ident_filter_new(void); + +extern int git_filter_buffered_stream_new( + git_writestream **out, + git_filter *filter, + int (*write_fn)(git_filter *, void **, git_str *, const git_str *, const git_filter_source *), + git_str *temp_buf, + void **payload, + const git_filter_source *source, + git_writestream *target); + +#endif diff --git a/src/libgit2/git2.rc b/src/libgit2/git2.rc new file mode 100644 index 0000000..d273afd --- /dev/null +++ b/src/libgit2/git2.rc @@ -0,0 +1,59 @@ +#include +#include "../../include/git2/version.h" + +#ifndef LIBGIT2_FILENAME +# ifdef __GNUC__ +# define LIBGIT2_FILENAME git2 +# else +# define LIBGIT2_FILENAME "git2" +# endif +#endif + +#ifndef LIBGIT2_COMMENTS +# define LIBGIT2_COMMENTS "For more information visit http://libgit2.github.com/" +#endif + +#ifdef __GNUC__ +# define _STR(x) #x +# define STR(x) _STR(x) +#else +# define STR(x) x +#endif + +#ifdef __GNUC__ +VS_VERSION_INFO VERSIONINFO +#else +VS_VERSION_INFO VERSIONINFO MOVEABLE IMPURE LOADONCALL DISCARDABLE +#endif + FILEVERSION LIBGIT2_VER_MAJOR,LIBGIT2_VER_MINOR,LIBGIT2_VER_REVISION,LIBGIT2_VER_PATCH + PRODUCTVERSION LIBGIT2_VER_MAJOR,LIBGIT2_VER_MINOR,LIBGIT2_VER_REVISION,LIBGIT2_VER_PATCH + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0 +#endif + FILEOS VOS_NT_WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + //language ID = U.S. English, char set = Windows, Multilingual + BEGIN + VALUE "FileDescription", "libgit2 - the Git linkable library\0" + VALUE "FileVersion", LIBGIT2_VERSION "\0" + VALUE "InternalName", STR(LIBGIT2_FILENAME) ".dll\0" + VALUE "LegalCopyright", "Copyright (C) the libgit2 contributors. All rights reserved.\0" + VALUE "OriginalFilename", STR(LIBGIT2_FILENAME) ".dll\0" + VALUE "ProductName", "libgit2\0" + VALUE "ProductVersion", LIBGIT2_VERSION "\0" + VALUE "Comments", LIBGIT2_COMMENTS "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/src/libgit2/grafts.c b/src/libgit2/grafts.c new file mode 100644 index 0000000..1d9373a --- /dev/null +++ b/src/libgit2/grafts.c @@ -0,0 +1,272 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "grafts.h" + +#include "futils.h" +#include "oid.h" +#include "oidarray.h" +#include "parse.h" + +struct git_grafts { + /* Map of `git_commit_graft`s */ + git_oidmap *commits; + + /* Type of object IDs */ + git_oid_t oid_type; + + /* File backing the graft. NULL if it's an in-memory graft */ + char *path; + unsigned char path_checksum[GIT_HASH_SHA256_SIZE]; +}; + +int git_grafts_new(git_grafts **out, git_oid_t oid_type) +{ + git_grafts *grafts; + + GIT_ASSERT_ARG(out && oid_type); + + grafts = git__calloc(1, sizeof(*grafts)); + GIT_ERROR_CHECK_ALLOC(grafts); + + if ((git_oidmap_new(&grafts->commits)) < 0) { + git__free(grafts); + return -1; + } + + grafts->oid_type = oid_type; + + *out = grafts; + return 0; +} + +int git_grafts_open( + git_grafts **out, + const char *path, + git_oid_t oid_type) +{ + git_grafts *grafts = NULL; + int error; + + GIT_ASSERT_ARG(out && path && oid_type); + + if ((error = git_grafts_new(&grafts, oid_type)) < 0) + goto error; + + grafts->path = git__strdup(path); + GIT_ERROR_CHECK_ALLOC(grafts->path); + + if ((error = git_grafts_refresh(grafts)) < 0) + goto error; + + *out = grafts; + +error: + if (error < 0) + git_grafts_free(grafts); + + return error; +} + +int git_grafts_open_or_refresh( + git_grafts **out, + const char *path, + git_oid_t oid_type) +{ + GIT_ASSERT_ARG(out && path && oid_type); + + return *out ? git_grafts_refresh(*out) : git_grafts_open(out, path, oid_type); +} + +void git_grafts_free(git_grafts *grafts) +{ + if (!grafts) + return; + git__free(grafts->path); + git_grafts_clear(grafts); + git_oidmap_free(grafts->commits); + git__free(grafts); +} + +void git_grafts_clear(git_grafts *grafts) +{ + git_commit_graft *graft; + + if (!grafts) + return; + + git_oidmap_foreach_value(grafts->commits, graft, { + git__free(graft->parents.ptr); + git__free(graft); + }); + + git_oidmap_clear(grafts->commits); +} + +int git_grafts_refresh(git_grafts *grafts) +{ + git_str contents = GIT_STR_INIT; + int error, updated = 0; + + GIT_ASSERT_ARG(grafts); + + if (!grafts->path) + return 0; + + if ((error = git_futils_readbuffer_updated(&contents, grafts->path, + grafts->path_checksum, &updated)) < 0) { + + if (error == GIT_ENOTFOUND) { + git_grafts_clear(grafts); + error = 0; + } + + goto cleanup; + } + + if (!updated) { + goto cleanup; + } + + if ((error = git_grafts_parse(grafts, contents.ptr, contents.size)) < 0) + goto cleanup; + +cleanup: + git_str_dispose(&contents); + return error; +} + +int git_grafts_parse(git_grafts *grafts, const char *buf, size_t len) +{ + git_array_oid_t parents = GIT_ARRAY_INIT; + git_parse_ctx parser; + int error; + + git_grafts_clear(grafts); + + if ((error = git_parse_ctx_init(&parser, buf, len)) < 0) + goto error; + + for (; parser.remain_len; git_parse_advance_line(&parser)) { + git_oid graft_oid; + + if ((error = git_parse_advance_oid(&graft_oid, &parser, grafts->oid_type)) < 0) { + git_error_set(GIT_ERROR_GRAFTS, "invalid graft OID at line %" PRIuZ, parser.line_num); + goto error; + } + + while (parser.line_len && git_parse_advance_expected(&parser, "\n", 1) != 0) { + git_oid *id = git_array_alloc(parents); + GIT_ERROR_CHECK_ALLOC(id); + + if ((error = git_parse_advance_expected(&parser, " ", 1)) < 0 || + (error = git_parse_advance_oid(id, &parser, grafts->oid_type)) < 0) { + git_error_set(GIT_ERROR_GRAFTS, "invalid parent OID at line %" PRIuZ, parser.line_num); + goto error; + } + } + + if ((error = git_grafts_add(grafts, &graft_oid, parents)) < 0) + goto error; + + git_array_clear(parents); + } + +error: + git_array_clear(parents); + return error; +} + +int git_grafts_add(git_grafts *grafts, const git_oid *oid, git_array_oid_t parents) +{ + git_commit_graft *graft; + git_oid *parent_oid; + int error; + size_t i; + + GIT_ASSERT_ARG(grafts && oid); + + graft = git__calloc(1, sizeof(*graft)); + GIT_ERROR_CHECK_ALLOC(graft); + + git_array_init_to_size(graft->parents, git_array_size(parents)); + git_array_foreach(parents, i, parent_oid) { + git_oid *id = git_array_alloc(graft->parents); + GIT_ERROR_CHECK_ALLOC(id); + + git_oid_cpy(id, parent_oid); + } + git_oid_cpy(&graft->oid, oid); + + if ((error = git_grafts_remove(grafts, &graft->oid)) < 0 && error != GIT_ENOTFOUND) + goto cleanup; + + if ((error = git_oidmap_set(grafts->commits, &graft->oid, graft)) < 0) + goto cleanup; + + return 0; + +cleanup: + git_array_clear(graft->parents); + git__free(graft); + return error; +} + +int git_grafts_remove(git_grafts *grafts, const git_oid *oid) +{ + git_commit_graft *graft; + int error; + + GIT_ASSERT_ARG(grafts && oid); + + if ((graft = git_oidmap_get(grafts->commits, oid)) == NULL) + return GIT_ENOTFOUND; + + if ((error = git_oidmap_delete(grafts->commits, oid)) < 0) + return error; + + git__free(graft->parents.ptr); + git__free(graft); + + return 0; +} + +int git_grafts_get(git_commit_graft **out, git_grafts *grafts, const git_oid *oid) +{ + GIT_ASSERT_ARG(out && grafts && oid); + if ((*out = git_oidmap_get(grafts->commits, oid)) == NULL) + return GIT_ENOTFOUND; + return 0; +} + +int git_grafts_oids(git_oid **out, size_t *out_len, git_grafts *grafts) +{ + git_array_oid_t array = GIT_ARRAY_INIT; + const git_oid *oid; + size_t existing, i = 0; + + GIT_ASSERT_ARG(out && grafts); + + if ((existing = git_oidmap_size(grafts->commits)) > 0) + git_array_init_to_size(array, existing); + + while (git_oidmap_iterate(NULL, grafts->commits, &i, &oid) == 0) { + git_oid *cpy = git_array_alloc(array); + GIT_ERROR_CHECK_ALLOC(cpy); + git_oid_cpy(cpy, oid); + } + + *out = array.ptr; + *out_len = array.size; + + return 0; +} + +size_t git_grafts_size(git_grafts *grafts) +{ + return git_oidmap_size(grafts->commits); +} diff --git a/src/libgit2/grafts.h b/src/libgit2/grafts.h new file mode 100644 index 0000000..394867f --- /dev/null +++ b/src/libgit2/grafts.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_graft_h__ +#define INCLUDE_graft_h__ + +#include "common.h" +#include "oidarray.h" +#include "oidmap.h" + +/** graft commit */ +typedef struct { + git_oid oid; + git_array_oid_t parents; +} git_commit_graft; + +typedef struct git_grafts git_grafts; + +int git_grafts_new(git_grafts **out, git_oid_t oid_type); +int git_grafts_open(git_grafts **out, const char *path, git_oid_t oid_type); +int git_grafts_open_or_refresh(git_grafts **out, const char *path, git_oid_t oid_type); +void git_grafts_free(git_grafts *grafts); +void git_grafts_clear(git_grafts *grafts); + +int git_grafts_refresh(git_grafts *grafts); +int git_grafts_parse(git_grafts *grafts, const char *buf, size_t len); +int git_grafts_add(git_grafts *grafts, const git_oid *oid, git_array_oid_t parents); +int git_grafts_remove(git_grafts *grafts, const git_oid *oid); +int git_grafts_get(git_commit_graft **out, git_grafts *grafts, const git_oid *oid); +int git_grafts_oids(git_oid **out, size_t *out_len, git_grafts *grafts); +size_t git_grafts_size(git_grafts *grafts); + +#endif diff --git a/src/libgit2/graph.c b/src/libgit2/graph.c new file mode 100644 index 0000000..35e914f --- /dev/null +++ b/src/libgit2/graph.c @@ -0,0 +1,249 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "revwalk.h" +#include "merge.h" +#include "git2/graph.h" + +static int interesting(git_pqueue *list, git_commit_list *roots) +{ + unsigned int i; + + for (i = 0; i < git_pqueue_size(list); i++) { + git_commit_list_node *commit = git_pqueue_get(list, i); + if ((commit->flags & STALE) == 0) + return 1; + } + + while(roots) { + if ((roots->item->flags & STALE) == 0) + return 1; + roots = roots->next; + } + + return 0; +} + +static int mark_parents(git_revwalk *walk, git_commit_list_node *one, + git_commit_list_node *two) +{ + unsigned int i; + git_commit_list *roots = NULL; + git_pqueue list; + + /* if the commit is repeated, we have a our merge base already */ + if (one == two) { + one->flags |= PARENT1 | PARENT2 | RESULT; + return 0; + } + + if (git_pqueue_init(&list, 0, 2, git_commit_list_generation_cmp) < 0) + return -1; + + if (git_commit_list_parse(walk, one) < 0) + goto on_error; + one->flags |= PARENT1; + if (git_pqueue_insert(&list, one) < 0) + goto on_error; + + if (git_commit_list_parse(walk, two) < 0) + goto on_error; + two->flags |= PARENT2; + if (git_pqueue_insert(&list, two) < 0) + goto on_error; + + /* as long as there are non-STALE commits */ + while (interesting(&list, roots)) { + git_commit_list_node *commit = git_pqueue_pop(&list); + unsigned int flags; + + if (commit == NULL) + break; + + flags = commit->flags & (PARENT1 | PARENT2 | STALE); + if (flags == (PARENT1 | PARENT2)) { + if (!(commit->flags & RESULT)) + commit->flags |= RESULT; + /* we mark the parents of a merge stale */ + flags |= STALE; + } + + for (i = 0; i < commit->out_degree; i++) { + git_commit_list_node *p = commit->parents[i]; + if ((p->flags & flags) == flags) + continue; + + if (git_commit_list_parse(walk, p) < 0) + goto on_error; + + p->flags |= flags; + if (git_pqueue_insert(&list, p) < 0) + goto on_error; + } + + /* Keep track of root commits, to make sure the path gets marked */ + if (commit->out_degree == 0) { + if (git_commit_list_insert(commit, &roots) == NULL) + goto on_error; + } + } + + git_commit_list_free(&roots); + git_pqueue_free(&list); + return 0; + +on_error: + git_commit_list_free(&roots); + git_pqueue_free(&list); + return -1; +} + + +static int ahead_behind(git_commit_list_node *one, git_commit_list_node *two, + size_t *ahead, size_t *behind) +{ + git_commit_list_node *commit; + git_pqueue pq; + int error = 0, i; + *ahead = 0; + *behind = 0; + + if (git_pqueue_init(&pq, 0, 2, git_commit_list_time_cmp) < 0) + return -1; + + if ((error = git_pqueue_insert(&pq, one)) < 0 || + (error = git_pqueue_insert(&pq, two)) < 0) + goto done; + + while ((commit = git_pqueue_pop(&pq)) != NULL) { + if (commit->flags & RESULT || + (commit->flags & (PARENT1 | PARENT2)) == (PARENT1 | PARENT2)) + continue; + else if (commit->flags & PARENT1) + (*ahead)++; + else if (commit->flags & PARENT2) + (*behind)++; + + for (i = 0; i < commit->out_degree; i++) { + git_commit_list_node *p = commit->parents[i]; + if ((error = git_pqueue_insert(&pq, p)) < 0) + goto done; + } + commit->flags |= RESULT; + } + +done: + git_pqueue_free(&pq); + return error; +} + +int git_graph_ahead_behind(size_t *ahead, size_t *behind, git_repository *repo, + const git_oid *local, const git_oid *upstream) +{ + git_revwalk *walk; + git_commit_list_node *commit_u, *commit_l; + + if (git_revwalk_new(&walk, repo) < 0) + return -1; + + commit_u = git_revwalk__commit_lookup(walk, upstream); + if (commit_u == NULL) + goto on_error; + + commit_l = git_revwalk__commit_lookup(walk, local); + if (commit_l == NULL) + goto on_error; + + if (mark_parents(walk, commit_l, commit_u) < 0) + goto on_error; + if (ahead_behind(commit_l, commit_u, ahead, behind) < 0) + goto on_error; + + git_revwalk_free(walk); + + return 0; + +on_error: + git_revwalk_free(walk); + return -1; +} + +int git_graph_descendant_of(git_repository *repo, const git_oid *commit, const git_oid *ancestor) +{ + if (git_oid_equal(commit, ancestor)) + return 0; + + return git_graph_reachable_from_any(repo, ancestor, commit, 1); +} + +int git_graph_reachable_from_any( + git_repository *repo, + const git_oid *commit_id, + const git_oid descendant_array[], + size_t length) +{ + git_revwalk *walk = NULL; + git_vector list; + git_commit_list *result = NULL; + git_commit_list_node *commit; + size_t i; + uint32_t minimum_generation = 0xffffffff; + int error = 0; + + if (!length) + return 0; + + for (i = 0; i < length; ++i) { + if (git_oid_equal(commit_id, &descendant_array[i])) + return 1; + } + + if ((error = git_vector_init(&list, length + 1, NULL)) < 0) + return error; + + if ((error = git_revwalk_new(&walk, repo)) < 0) + goto done; + + for (i = 0; i < length; i++) { + commit = git_revwalk__commit_lookup(walk, &descendant_array[i]); + if (commit == NULL) { + error = -1; + goto done; + } + + git_vector_insert(&list, commit); + if (minimum_generation > commit->generation) + minimum_generation = commit->generation; + } + + commit = git_revwalk__commit_lookup(walk, commit_id); + if (commit == NULL) { + error = -1; + goto done; + } + + if (minimum_generation > commit->generation) + minimum_generation = commit->generation; + + if ((error = git_merge__bases_many(&result, walk, commit, &list, minimum_generation)) < 0) + goto done; + + if (result) { + error = git_oid_equal(commit_id, &result->item->oid); + } else { + /* No merge-base found, it's not a descendant */ + error = 0; + } + +done: + git_commit_list_free(&result); + git_vector_free(&list); + git_revwalk_free(walk); + return error; +} diff --git a/src/libgit2/hashsig.c b/src/libgit2/hashsig.c new file mode 100644 index 0000000..6b4fb83 --- /dev/null +++ b/src/libgit2/hashsig.c @@ -0,0 +1,375 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "git2/sys/hashsig.h" +#include "futils.h" +#include "util.h" + +typedef uint32_t hashsig_t; +typedef uint64_t hashsig_state; + +#define HASHSIG_SCALE 100 + +#define HASHSIG_MAX_RUN 80 +#define HASHSIG_HASH_START INT64_C(0x012345678ABCDEF0) +#define HASHSIG_HASH_SHIFT 5 + +#define HASHSIG_HASH_MIX(S,CH) \ + (S) = ((S) << HASHSIG_HASH_SHIFT) - (S) + (hashsig_state)(CH) + +#define HASHSIG_HEAP_SIZE ((1 << 7) - 1) +#define HASHSIG_HEAP_MIN_SIZE 4 + +typedef int (*hashsig_cmp)(const void *a, const void *b, void *); + +typedef struct { + int size, asize; + hashsig_cmp cmp; + hashsig_t values[HASHSIG_HEAP_SIZE]; +} hashsig_heap; + +struct git_hashsig { + hashsig_heap mins; + hashsig_heap maxs; + size_t lines; + git_hashsig_option_t opt; +}; + +#define HEAP_LCHILD_OF(I) (((I)<<1)+1) +#define HEAP_RCHILD_OF(I) (((I)<<1)+2) +#define HEAP_PARENT_OF(I) (((I)-1)>>1) + +static void hashsig_heap_init(hashsig_heap *h, hashsig_cmp cmp) +{ + h->size = 0; + h->asize = HASHSIG_HEAP_SIZE; + h->cmp = cmp; +} + +static int hashsig_cmp_max(const void *a, const void *b, void *payload) +{ + hashsig_t av = *(const hashsig_t *)a, bv = *(const hashsig_t *)b; + GIT_UNUSED(payload); + return (av < bv) ? -1 : (av > bv) ? 1 : 0; +} + +static int hashsig_cmp_min(const void *a, const void *b, void *payload) +{ + hashsig_t av = *(const hashsig_t *)a, bv = *(const hashsig_t *)b; + GIT_UNUSED(payload); + return (av > bv) ? -1 : (av < bv) ? 1 : 0; +} + +static void hashsig_heap_up(hashsig_heap *h, int el) +{ + int parent_el = HEAP_PARENT_OF(el); + + while (el > 0 && h->cmp(&h->values[parent_el], &h->values[el], NULL) > 0) { + hashsig_t t = h->values[el]; + h->values[el] = h->values[parent_el]; + h->values[parent_el] = t; + + el = parent_el; + parent_el = HEAP_PARENT_OF(el); + } +} + +static void hashsig_heap_down(hashsig_heap *h, int el) +{ + hashsig_t v, lv, rv; + + /* 'el < h->size / 2' tests if el is bottom row of heap */ + + while (el < h->size / 2) { + int lel = HEAP_LCHILD_OF(el), rel = HEAP_RCHILD_OF(el), swapel; + + v = h->values[el]; + lv = h->values[lel]; + rv = h->values[rel]; + + if (h->cmp(&v, &lv, NULL) < 0 && h->cmp(&v, &rv, NULL) < 0) + break; + + swapel = (h->cmp(&lv, &rv, NULL) < 0) ? lel : rel; + + h->values[el] = h->values[swapel]; + h->values[swapel] = v; + + el = swapel; + } +} + +static void hashsig_heap_sort(hashsig_heap *h) +{ + /* only need to do this at the end for signature comparison */ + git__qsort_r(h->values, h->size, sizeof(hashsig_t), h->cmp, NULL); +} + +static void hashsig_heap_insert(hashsig_heap *h, hashsig_t val) +{ + /* if heap is not full, insert new element */ + if (h->size < h->asize) { + h->values[h->size++] = val; + hashsig_heap_up(h, h->size - 1); + } + + /* if heap is full, pop top if new element should replace it */ + else if (h->cmp(&val, &h->values[0], NULL) > 0) { + h->size--; + h->values[0] = h->values[h->size]; + hashsig_heap_down(h, 0); + } + +} + +typedef struct { + int use_ignores; + uint8_t ignore_ch[256]; +} hashsig_in_progress; + +static int hashsig_in_progress_init( + hashsig_in_progress *prog, git_hashsig *sig) +{ + int i; + + /* no more than one can be set */ + GIT_ASSERT(!(sig->opt & GIT_HASHSIG_IGNORE_WHITESPACE) || + !(sig->opt & GIT_HASHSIG_SMART_WHITESPACE)); + + if (sig->opt & GIT_HASHSIG_IGNORE_WHITESPACE) { + for (i = 0; i < 256; ++i) + prog->ignore_ch[i] = git__isspace_nonlf(i); + prog->use_ignores = 1; + } else if (sig->opt & GIT_HASHSIG_SMART_WHITESPACE) { + for (i = 0; i < 256; ++i) + prog->ignore_ch[i] = git__isspace(i); + prog->use_ignores = 1; + } else { + memset(prog, 0, sizeof(*prog)); + } + + return 0; +} + +static int hashsig_add_hashes( + git_hashsig *sig, + const uint8_t *data, + size_t size, + hashsig_in_progress *prog) +{ + const uint8_t *scan = data, *end = data + size; + hashsig_state state = HASHSIG_HASH_START; + int use_ignores = prog->use_ignores, len; + uint8_t ch; + + while (scan < end) { + state = HASHSIG_HASH_START; + + for (len = 0; scan < end && len < HASHSIG_MAX_RUN; ) { + ch = *scan; + + if (use_ignores) + for (; scan < end && git__isspace_nonlf(ch); ch = *scan) + ++scan; + else if (sig->opt & + (GIT_HASHSIG_IGNORE_WHITESPACE | GIT_HASHSIG_SMART_WHITESPACE)) + for (; scan < end && ch == '\r'; ch = *scan) + ++scan; + + /* peek at next character to decide what to do next */ + if (sig->opt & GIT_HASHSIG_SMART_WHITESPACE) + use_ignores = (ch == '\n'); + + if (scan >= end) + break; + ++scan; + + /* check run terminator */ + if (ch == '\n' || ch == '\0') { + sig->lines++; + break; + } + + ++len; + HASHSIG_HASH_MIX(state, ch); + } + + if (len > 0) { + hashsig_heap_insert(&sig->mins, (hashsig_t)state); + hashsig_heap_insert(&sig->maxs, (hashsig_t)state); + + while (scan < end && (*scan == '\n' || !*scan)) + ++scan; + } + } + + prog->use_ignores = use_ignores; + + return 0; +} + +static int hashsig_finalize_hashes(git_hashsig *sig) +{ + if (sig->mins.size < HASHSIG_HEAP_MIN_SIZE && + !(sig->opt & GIT_HASHSIG_ALLOW_SMALL_FILES)) { + git_error_set(GIT_ERROR_INVALID, + "file too small for similarity signature calculation"); + return GIT_EBUFS; + } + + hashsig_heap_sort(&sig->mins); + hashsig_heap_sort(&sig->maxs); + + return 0; +} + +static git_hashsig *hashsig_alloc(git_hashsig_option_t opts) +{ + git_hashsig *sig = git__calloc(1, sizeof(git_hashsig)); + if (!sig) + return NULL; + + hashsig_heap_init(&sig->mins, hashsig_cmp_min); + hashsig_heap_init(&sig->maxs, hashsig_cmp_max); + sig->opt = opts; + + return sig; +} + +int git_hashsig_create( + git_hashsig **out, + const char *buf, + size_t buflen, + git_hashsig_option_t opts) +{ + int error; + hashsig_in_progress prog; + git_hashsig *sig = hashsig_alloc(opts); + GIT_ERROR_CHECK_ALLOC(sig); + + if ((error = hashsig_in_progress_init(&prog, sig)) < 0) + return error; + + error = hashsig_add_hashes(sig, (const uint8_t *)buf, buflen, &prog); + + if (!error) + error = hashsig_finalize_hashes(sig); + + if (!error) + *out = sig; + else + git_hashsig_free(sig); + + return error; +} + +int git_hashsig_create_fromfile( + git_hashsig **out, + const char *path, + git_hashsig_option_t opts) +{ + uint8_t buf[0x1000]; + ssize_t buflen = 0; + int error = 0, fd; + hashsig_in_progress prog; + git_hashsig *sig = hashsig_alloc(opts); + GIT_ERROR_CHECK_ALLOC(sig); + + if ((fd = git_futils_open_ro(path)) < 0) { + git__free(sig); + return fd; + } + + if ((error = hashsig_in_progress_init(&prog, sig)) < 0) { + p_close(fd); + return error; + } + + while (!error) { + if ((buflen = p_read(fd, buf, sizeof(buf))) <= 0) { + if ((error = (int)buflen) < 0) + git_error_set(GIT_ERROR_OS, + "read error on '%s' calculating similarity hashes", path); + break; + } + + error = hashsig_add_hashes(sig, buf, buflen, &prog); + } + + p_close(fd); + + if (!error) + error = hashsig_finalize_hashes(sig); + + if (!error) + *out = sig; + else + git_hashsig_free(sig); + + return error; +} + +void git_hashsig_free(git_hashsig *sig) +{ + git__free(sig); +} + +static int hashsig_heap_compare(const hashsig_heap *a, const hashsig_heap *b) +{ + int matches = 0, i, j, cmp; + + GIT_ASSERT_WITH_RETVAL(a->cmp == b->cmp, 0); + + /* hash heaps are sorted - just look for overlap vs total */ + + for (i = 0, j = 0; i < a->size && j < b->size; ) { + cmp = a->cmp(&a->values[i], &b->values[j], NULL); + + if (cmp < 0) + ++i; + else if (cmp > 0) + ++j; + else { + ++i; ++j; ++matches; + } + } + + return HASHSIG_SCALE * (matches * 2) / (a->size + b->size); +} + +int git_hashsig_compare(const git_hashsig *a, const git_hashsig *b) +{ + /* if we have no elements in either file then each file is either + * empty or blank. if we're ignoring whitespace then the files are + * similar, otherwise they're dissimilar. + */ + if (a->mins.size == 0 && b->mins.size == 0) { + if ((!a->lines && !b->lines) || + (a->opt & GIT_HASHSIG_IGNORE_WHITESPACE)) + return HASHSIG_SCALE; + else + return 0; + } + + /* if we have fewer than the maximum number of elements, then just use + * one array since the two arrays will be the same + */ + if (a->mins.size < HASHSIG_HEAP_SIZE) { + return hashsig_heap_compare(&a->mins, &b->mins); + } else { + int mins, maxs; + + if ((mins = hashsig_heap_compare(&a->mins, &b->mins)) < 0) + return mins; + if ((maxs = hashsig_heap_compare(&a->maxs, &b->maxs)) < 0) + return maxs; + + return (mins + maxs) / 2; + } +} diff --git a/src/libgit2/ident.c b/src/libgit2/ident.c new file mode 100644 index 0000000..97110c6 --- /dev/null +++ b/src/libgit2/ident.c @@ -0,0 +1,139 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "git2/sys/filter.h" +#include "filter.h" +#include "str.h" + +static int ident_find_id( + const char **id_start, const char **id_end, const char *start, size_t len) +{ + const char *end = start + len, *found = NULL; + + while (len > 3 && (found = memchr(start, '$', len)) != NULL) { + size_t remaining = (size_t)(end - found) - 1; + if (remaining < 3) + return GIT_ENOTFOUND; + + start = found + 1; + len = remaining; + + if (start[0] == 'I' && start[1] == 'd') + break; + } + + if (len < 3 || !found) + return GIT_ENOTFOUND; + *id_start = found; + + if ((found = memchr(start + 2, '$', len - 2)) == NULL) + return GIT_ENOTFOUND; + + *id_end = found + 1; + return 0; +} + +static int ident_insert_id( + git_str *to, const git_str *from, const git_filter_source *src) +{ + char oid[GIT_OID_MAX_HEXSIZE + 1]; + const char *id_start, *id_end, *from_end = from->ptr + from->size; + size_t need_size; + + /* replace $Id$ with blob id */ + + if (!git_filter_source_id(src)) + return GIT_PASSTHROUGH; + + git_oid_tostr(oid, sizeof(oid), git_filter_source_id(src)); + + if (ident_find_id(&id_start, &id_end, from->ptr, from->size) < 0) + return GIT_PASSTHROUGH; + + need_size = (size_t)(id_start - from->ptr) + + 5 /* "$Id: " */ + GIT_OID_MAX_HEXSIZE + 2 /* " $" */ + + (size_t)(from_end - id_end); + + if (git_str_grow(to, need_size) < 0) + return -1; + + git_str_set(to, from->ptr, (size_t)(id_start - from->ptr)); + git_str_put(to, "$Id: ", 5); + git_str_puts(to, oid); + git_str_put(to, " $", 2); + git_str_put(to, id_end, (size_t)(from_end - id_end)); + + return git_str_oom(to) ? -1 : 0; +} + +static int ident_remove_id( + git_str *to, const git_str *from) +{ + const char *id_start, *id_end, *from_end = from->ptr + from->size; + size_t need_size; + + if (ident_find_id(&id_start, &id_end, from->ptr, from->size) < 0) + return GIT_PASSTHROUGH; + + need_size = (size_t)(id_start - from->ptr) + + 4 /* "$Id$" */ + (size_t)(from_end - id_end); + + if (git_str_grow(to, need_size) < 0) + return -1; + + git_str_set(to, from->ptr, (size_t)(id_start - from->ptr)); + git_str_put(to, "$Id$", 4); + git_str_put(to, id_end, (size_t)(from_end - id_end)); + + return git_str_oom(to) ? -1 : 0; +} + +static int ident_apply( + git_filter *self, + void **payload, + git_str *to, + const git_str *from, + const git_filter_source *src) +{ + GIT_UNUSED(self); GIT_UNUSED(payload); + + /* Don't filter binary files */ + if (git_str_is_binary(from)) + return GIT_PASSTHROUGH; + + if (git_filter_source_mode(src) == GIT_FILTER_SMUDGE) + return ident_insert_id(to, from, src); + else + return ident_remove_id(to, from); +} + +static int ident_stream( + git_writestream **out, + git_filter *self, + void **payload, + const git_filter_source *src, + git_writestream *next) +{ + return git_filter_buffered_stream_new(out, + self, ident_apply, NULL, payload, src, next); +} + +git_filter *git_ident_filter_new(void) +{ + git_filter *f = git__calloc(1, sizeof(git_filter)); + if (f == NULL) + return NULL; + + f->version = GIT_FILTER_VERSION; + f->attributes = "+ident"; /* apply to files with ident attribute set */ + f->shutdown = git_filter_free; + f->stream = ident_stream; + + return f; +} diff --git a/src/libgit2/idxmap.c b/src/libgit2/idxmap.c new file mode 100644 index 0000000..bc23608 --- /dev/null +++ b/src/libgit2/idxmap.c @@ -0,0 +1,157 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "idxmap.h" + +#define kmalloc git__malloc +#define kcalloc git__calloc +#define krealloc git__realloc +#define kreallocarray git__reallocarray +#define kfree git__free +#include "khash.h" + +__KHASH_TYPE(idx, const git_index_entry *, git_index_entry *) +__KHASH_TYPE(idxicase, const git_index_entry *, git_index_entry *) + +/* This is __ac_X31_hash_string but with tolower and it takes the entry's stage into account */ +static kh_inline khint_t idxentry_hash(const git_index_entry *e) +{ + const char *s = e->path; + khint_t h = (khint_t)git__tolower(*s); + if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)git__tolower(*s); + return h + GIT_INDEX_ENTRY_STAGE(e); +} + +#define idxentry_equal(a, b) (GIT_INDEX_ENTRY_STAGE(a) == GIT_INDEX_ENTRY_STAGE(b) && strcmp(a->path, b->path) == 0) +#define idxentry_icase_equal(a, b) (GIT_INDEX_ENTRY_STAGE(a) == GIT_INDEX_ENTRY_STAGE(b) && strcasecmp(a->path, b->path) == 0) + +__KHASH_IMPL(idx, static kh_inline, const git_index_entry *, git_index_entry *, 1, idxentry_hash, idxentry_equal) +__KHASH_IMPL(idxicase, static kh_inline, const git_index_entry *, git_index_entry *, 1, idxentry_hash, idxentry_icase_equal) + +int git_idxmap_new(git_idxmap **out) +{ + *out = kh_init(idx); + GIT_ERROR_CHECK_ALLOC(*out); + + return 0; +} + +int git_idxmap_icase_new(git_idxmap_icase **out) +{ + *out = kh_init(idxicase); + GIT_ERROR_CHECK_ALLOC(*out); + + return 0; +} + +void git_idxmap_free(git_idxmap *map) +{ + kh_destroy(idx, map); +} + +void git_idxmap_icase_free(git_idxmap_icase *map) +{ + kh_destroy(idxicase, map); +} + +void git_idxmap_clear(git_idxmap *map) +{ + kh_clear(idx, map); +} + +void git_idxmap_icase_clear(git_idxmap_icase *map) +{ + kh_clear(idxicase, map); +} + +int git_idxmap_resize(git_idxmap *map, size_t size) +{ + if (!git__is_uint32(size) || + kh_resize(idx, map, (khiter_t)size) < 0) { + git_error_set_oom(); + return -1; + } + return 0; +} + +int git_idxmap_icase_resize(git_idxmap_icase *map, size_t size) +{ + if (!git__is_uint32(size) || + kh_resize(idxicase, map, (khiter_t)size) < 0) { + git_error_set_oom(); + return -1; + } + return 0; +} + +void *git_idxmap_get(git_idxmap *map, const git_index_entry *key) +{ + size_t idx = kh_get(idx, map, key); + if (idx == kh_end(map) || !kh_exist(map, idx)) + return NULL; + return kh_val(map, idx); +} + +int git_idxmap_set(git_idxmap *map, const git_index_entry *key, void *value) +{ + size_t idx; + int rval; + + idx = kh_put(idx, map, key, &rval); + if (rval < 0) + return -1; + + if (rval == 0) + kh_key(map, idx) = key; + + kh_val(map, idx) = value; + + return 0; +} + +int git_idxmap_icase_set(git_idxmap_icase *map, const git_index_entry *key, void *value) +{ + size_t idx; + int rval; + + idx = kh_put(idxicase, map, key, &rval); + if (rval < 0) + return -1; + + if (rval == 0) + kh_key(map, idx) = key; + + kh_val(map, idx) = value; + + return 0; +} + +void *git_idxmap_icase_get(git_idxmap_icase *map, const git_index_entry *key) +{ + size_t idx = kh_get(idxicase, map, key); + if (idx == kh_end(map) || !kh_exist(map, idx)) + return NULL; + return kh_val(map, idx); +} + +int git_idxmap_delete(git_idxmap *map, const git_index_entry *key) +{ + khiter_t idx = kh_get(idx, map, key); + if (idx == kh_end(map)) + return GIT_ENOTFOUND; + kh_del(idx, map, idx); + return 0; +} + +int git_idxmap_icase_delete(git_idxmap_icase *map, const git_index_entry *key) +{ + khiter_t idx = kh_get(idxicase, map, key); + if (idx == kh_end(map)) + return GIT_ENOTFOUND; + kh_del(idxicase, map, idx); + return 0; +} diff --git a/src/libgit2/idxmap.h b/src/libgit2/idxmap.h new file mode 100644 index 0000000..76170ef --- /dev/null +++ b/src/libgit2/idxmap.h @@ -0,0 +1,177 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_idxmap_h__ +#define INCLUDE_idxmap_h__ + +#include "common.h" + +#include "git2/index.h" + +/** A map with `git_index_entry`s as key. */ +typedef struct kh_idx_s git_idxmap; +/** A map with case-insensitive `git_index_entry`s as key */ +typedef struct kh_idxicase_s git_idxmap_icase; + +/** + * Allocate a new index entry map. + * + * @param out Pointer to the map that shall be allocated. + * @return 0 on success, an error code if allocation has failed. + */ +int git_idxmap_new(git_idxmap **out); + +/** + * Allocate a new case-insensitive index entry map. + * + * @param out Pointer to the map that shall be allocated. + * @return 0 on success, an error code if allocation has failed. + */ +int git_idxmap_icase_new(git_idxmap_icase **out); + +/** + * Free memory associated with the map. + * + * Note that this function will _not_ free values added to this + * map. + * + * @param map Pointer to the map that is to be free'd. May be + * `NULL`. + */ +void git_idxmap_free(git_idxmap *map); + +/** + * Free memory associated with the map. + * + * Note that this function will _not_ free values added to this + * map. + * + * @param map Pointer to the map that is to be free'd. May be + * `NULL`. + */ +void git_idxmap_icase_free(git_idxmap_icase *map); + +/** + * Clear all entries from the map. + * + * This function will remove all entries from the associated map. + * Memory associated with it will not be released, though. + * + * @param map Pointer to the map that shall be cleared. May be + * `NULL`. + */ +void git_idxmap_clear(git_idxmap *map); + +/** + * Clear all entries from the map. + * + * This function will remove all entries from the associated map. + * Memory associated with it will not be released, though. + * + * @param map Pointer to the map that shall be cleared. May be + * `NULL`. + */ +void git_idxmap_icase_clear(git_idxmap_icase *map); + +/** + * Resize the map by allocating more memory. + * + * @param map map that shall be resized + * @param size count of entries that the map shall hold + * @return `0` if the map was successfully resized, a negative + * error code otherwise + */ +int git_idxmap_resize(git_idxmap *map, size_t size); + +/** + * Resize the map by allocating more memory. + * + * @param map map that shall be resized + * @param size count of entries that the map shall hold + * @return `0` if the map was successfully resized, a negative + * error code otherwise + */ +int git_idxmap_icase_resize(git_idxmap_icase *map, size_t size); + +/** + * Return value associated with the given key. + * + * @param map map to search key in + * @param key key to search for; the index entry will be searched + * for by its case-sensitive path + * @return value associated with the given key or NULL if the key was not found + */ +void *git_idxmap_get(git_idxmap *map, const git_index_entry *key); + +/** + * Return value associated with the given key. + * + * @param map map to search key in + * @param key key to search for; the index entry will be searched + * for by its case-insensitive path + * @return value associated with the given key or NULL if the key was not found + */ +void *git_idxmap_icase_get(git_idxmap_icase *map, const git_index_entry *key); + +/** + * Set the entry for key to value. + * + * If the map has no corresponding entry for the given key, a new + * entry will be created with the given value. If an entry exists + * already, its value will be updated to match the given value. + * + * @param map map to create new entry in + * @param key key to set + * @param value value to associate the key with; may be NULL + * @return zero if the key was successfully set, a negative error + * code otherwise + */ +int git_idxmap_set(git_idxmap *map, const git_index_entry *key, void *value); + +/** + * Set the entry for key to value. + * + * If the map has no corresponding entry for the given key, a new + * entry will be created with the given value. If an entry exists + * already, its value will be updated to match the given value. + * + * @param map map to create new entry in + * @param key key to set + * @param value value to associate the key with; may be NULL + * @return zero if the key was successfully set, a negative error + * code otherwise + */ +int git_idxmap_icase_set(git_idxmap_icase *map, const git_index_entry *key, void *value); + +/** + * Delete an entry from the map. + * + * Delete the given key and its value from the map. If no such + * key exists, this will do nothing. + * + * @param map map to delete key in + * @param key key to delete + * @return `0` if the key has been deleted, GIT_ENOTFOUND if no + * such key was found, a negative code in case of an + * error + */ +int git_idxmap_delete(git_idxmap *map, const git_index_entry *key); + +/** + * Delete an entry from the map. + * + * Delete the given key and its value from the map. If no such + * key exists, this will do nothing. + * + * @param map map to delete key in + * @param key key to delete + * @return `0` if the key has been deleted, GIT_ENOTFOUND if no + * such key was found, a negative code in case of an + * error + */ +int git_idxmap_icase_delete(git_idxmap_icase *map, const git_index_entry *key); + +#endif diff --git a/src/libgit2/ignore.c b/src/libgit2/ignore.c new file mode 100644 index 0000000..cee58d7 --- /dev/null +++ b/src/libgit2/ignore.c @@ -0,0 +1,652 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "ignore.h" + +#include "git2/ignore.h" +#include "common.h" +#include "attrcache.h" +#include "fs_path.h" +#include "config.h" +#include "wildmatch.h" +#include "path.h" + +#define GIT_IGNORE_INTERNAL "[internal]exclude" + +#define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n" + +/** + * A negative ignore pattern can negate a positive one without + * wildcards if it is a basename only and equals the basename of + * the positive pattern. Thus + * + * foo/bar + * !bar + * + * would result in foo/bar being unignored again while + * + * moo/foo/bar + * !foo/bar + * + * would do nothing. The reverse also holds true: a positive + * basename pattern can be negated by unignoring the basename in + * subdirectories. Thus + * + * bar + * !foo/bar + * + * would result in foo/bar being unignored again. As with the + * first case, + * + * foo/bar + * !moo/foo/bar + * + * would do nothing, again. + */ +static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg) +{ + int (*cmp)(const char *, const char *, size_t); + git_attr_fnmatch *longer, *shorter; + char *p; + + if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0 + || (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) + return false; + + if (neg->flags & GIT_ATTR_FNMATCH_ICASE) + cmp = git__strncasecmp; + else + cmp = git__strncmp; + + /* If lengths match we need to have an exact match */ + if (rule->length == neg->length) { + return cmp(rule->pattern, neg->pattern, rule->length) == 0; + } else if (rule->length < neg->length) { + shorter = rule; + longer = neg; + } else { + shorter = neg; + longer = rule; + } + + /* Otherwise, we need to check if the shorter + * rule is a basename only (that is, it contains + * no path separator) and, if so, if it + * matches the tail of the longer rule */ + p = longer->pattern + longer->length - shorter->length; + + if (p[-1] != '/') + return false; + if (memchr(shorter->pattern, '/', shorter->length) != NULL) + return false; + + return cmp(p, shorter->pattern, shorter->length) == 0; +} + +/** + * A negative ignore can only unignore a file which is given explicitly before, thus + * + * foo + * !foo/bar + * + * does not unignore 'foo/bar' as it's not in the list. However + * + * foo/ + * !foo/bar + * + * does unignore 'foo/bar', as it is contained within the 'foo/' rule. + */ +static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match) +{ + int error = 0, wildmatch_flags, effective_flags; + size_t i; + git_attr_fnmatch *rule; + char *path; + git_str buf = GIT_STR_INIT; + + *out = 0; + + wildmatch_flags = WM_PATHNAME; + if (match->flags & GIT_ATTR_FNMATCH_ICASE) + wildmatch_flags |= WM_CASEFOLD; + + /* path of the file relative to the workdir, so we match the rules in subdirs */ + if (match->containing_dir) { + git_str_puts(&buf, match->containing_dir); + } + if (git_str_puts(&buf, match->pattern) < 0) + return -1; + + path = git_str_detach(&buf); + + git_vector_foreach(rules, i, rule) { + if (!(rule->flags & GIT_ATTR_FNMATCH_HASWILD)) { + if (does_negate_pattern(rule, match)) { + error = 0; + *out = 1; + goto out; + } + else + continue; + } + + git_str_clear(&buf); + if (rule->containing_dir) + git_str_puts(&buf, rule->containing_dir); + git_str_puts(&buf, rule->pattern); + + if (git_str_oom(&buf)) + goto out; + + /* + * if rule isn't for full path we match without PATHNAME flag + * as lines like *.txt should match something like dir/test.txt + * requiring * to also match / + */ + effective_flags = wildmatch_flags; + if (!(rule->flags & GIT_ATTR_FNMATCH_FULLPATH)) + effective_flags &= ~WM_PATHNAME; + + /* if we found a match, we want to keep this rule */ + if ((wildmatch(git_str_cstr(&buf), path, effective_flags)) == WM_MATCH) { + *out = 1; + error = 0; + goto out; + } + } + + error = 0; + +out: + git__free(path); + git_str_dispose(&buf); + return error; +} + +static int parse_ignore_file( + git_repository *repo, git_attr_file *attrs, const char *data, bool allow_macros) +{ + int error = 0; + int ignore_case = false; + const char *scan = data, *context = NULL; + git_attr_fnmatch *match = NULL; + + GIT_UNUSED(allow_macros); + + if (git_repository__configmap_lookup(&ignore_case, repo, GIT_CONFIGMAP_IGNORECASE) < 0) + git_error_clear(); + + /* if subdir file path, convert context for file paths */ + if (attrs->entry && + git_fs_path_root(attrs->entry->path) < 0 && + !git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE)) + context = attrs->entry->path; + + if (git_mutex_lock(&attrs->lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock ignore file"); + return -1; + } + + while (!error && *scan) { + int valid_rule = 1; + + if (!match && !(match = git__calloc(1, sizeof(*match)))) { + error = -1; + break; + } + + match->flags = + GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG; + + if (!(error = git_attr_fnmatch__parse( + match, &attrs->pool, context, &scan))) + { + match->flags |= GIT_ATTR_FNMATCH_IGNORE; + + if (ignore_case) + match->flags |= GIT_ATTR_FNMATCH_ICASE; + + scan = git__next_line(scan); + + /* + * If a negative match doesn't actually do anything, + * throw it away. As we cannot always verify whether a + * rule containing wildcards negates another rule, we + * do not optimize away these rules, though. + * */ + if (match->flags & GIT_ATTR_FNMATCH_NEGATIVE + && !(match->flags & GIT_ATTR_FNMATCH_HASWILD)) + error = does_negate_rule(&valid_rule, &attrs->rules, match); + + if (!error && valid_rule) + error = git_vector_insert(&attrs->rules, match); + } + + if (error != 0 || !valid_rule) { + match->pattern = NULL; + + if (error == GIT_ENOTFOUND) + error = 0; + } else { + match = NULL; /* vector now "owns" the match */ + } + } + + git_mutex_unlock(&attrs->lock); + git__free(match); + + return error; +} + +static int push_ignore_file( + git_ignores *ignores, + git_vector *which_list, + const char *base, + const char *filename) +{ + git_attr_file_source source = { GIT_ATTR_FILE_SOURCE_FILE, base, filename }; + git_attr_file *file = NULL; + int error = 0; + + error = git_attr_cache__get(&file, ignores->repo, NULL, &source, parse_ignore_file, false); + + if (error < 0) + return error; + + if (file != NULL) { + if ((error = git_vector_insert(which_list, file)) < 0) + git_attr_file__free(file); + } + + return error; +} + +static int push_one_ignore(void *payload, const char *path) +{ + git_ignores *ign = payload; + ign->depth++; + return push_ignore_file(ign, &ign->ign_path, path, GIT_IGNORE_FILE); +} + +static int get_internal_ignores(git_attr_file **out, git_repository *repo) +{ + git_attr_file_source source = { GIT_ATTR_FILE_SOURCE_MEMORY, NULL, GIT_IGNORE_INTERNAL }; + int error; + + if ((error = git_attr_cache__init(repo)) < 0) + return error; + + error = git_attr_cache__get(out, repo, NULL, &source, NULL, false); + + /* if internal rules list is empty, insert default rules */ + if (!error && !(*out)->rules.length) + error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES, false); + + return error; +} + +int git_ignore__for_path( + git_repository *repo, + const char *path, + git_ignores *ignores) +{ + int error = 0; + const char *workdir = git_repository_workdir(repo); + git_str infopath = GIT_STR_INIT; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(ignores); + GIT_ASSERT_ARG(path); + + memset(ignores, 0, sizeof(*ignores)); + ignores->repo = repo; + + /* Read the ignore_case flag */ + if ((error = git_repository__configmap_lookup( + &ignores->ignore_case, repo, GIT_CONFIGMAP_IGNORECASE)) < 0) + goto cleanup; + + if ((error = git_attr_cache__init(repo)) < 0) + goto cleanup; + + /* given a unrooted path in a non-bare repo, resolve it */ + if (workdir && git_fs_path_root(path) < 0) { + git_str local = GIT_STR_INIT; + + if ((error = git_fs_path_dirname_r(&local, path)) < 0 || + (error = git_fs_path_resolve_relative(&local, 0)) < 0 || + (error = git_fs_path_to_dir(&local)) < 0 || + (error = git_str_joinpath(&ignores->dir, workdir, local.ptr)) < 0 || + (error = git_path_validate_str_length(repo, &ignores->dir)) < 0) { + /* Nothing, we just want to stop on the first error */ + } + + git_str_dispose(&local); + } else { + if (!(error = git_str_joinpath(&ignores->dir, path, ""))) + error = git_path_validate_str_length(NULL, &ignores->dir); + } + + if (error < 0) + goto cleanup; + + if (workdir && !git__prefixcmp(ignores->dir.ptr, workdir)) + ignores->dir_root = strlen(workdir); + + /* set up internals */ + if ((error = get_internal_ignores(&ignores->ign_internal, repo)) < 0) + goto cleanup; + + /* load .gitignore up the path */ + if (workdir != NULL) { + error = git_fs_path_walk_up( + &ignores->dir, workdir, push_one_ignore, ignores); + if (error < 0) + goto cleanup; + } + + /* load .git/info/exclude if possible */ + if ((error = git_repository__item_path(&infopath, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 || + (error = push_ignore_file(ignores, &ignores->ign_global, infopath.ptr, GIT_IGNORE_FILE_INREPO)) < 0) { + if (error != GIT_ENOTFOUND) + goto cleanup; + error = 0; + } + + /* load core.excludesfile */ + if (git_repository_attr_cache(repo)->cfg_excl_file != NULL) + error = push_ignore_file( + ignores, &ignores->ign_global, NULL, + git_repository_attr_cache(repo)->cfg_excl_file); + +cleanup: + git_str_dispose(&infopath); + if (error < 0) + git_ignore__free(ignores); + + return error; +} + +int git_ignore__push_dir(git_ignores *ign, const char *dir) +{ + if (git_str_joinpath(&ign->dir, ign->dir.ptr, dir) < 0) + return -1; + + ign->depth++; + + return push_ignore_file( + ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE); +} + +int git_ignore__pop_dir(git_ignores *ign) +{ + if (ign->ign_path.length > 0) { + git_attr_file *file = git_vector_last(&ign->ign_path); + const char *start = file->entry->path, *end; + + /* - ign->dir looks something like "/home/user/a/b/" (or "a/b/c/d/") + * - file->path looks something like "a/b/.gitignore + * + * We are popping the last directory off ign->dir. We also want + * to remove the file from the vector if the popped directory + * matches the ignore path. We need to test if the "a/b" part of + * the file key matches the path we are about to pop. + */ + + if ((end = strrchr(start, '/')) != NULL) { + size_t dirlen = (end - start) + 1; + const char *relpath = ign->dir.ptr + ign->dir_root; + size_t pathlen = ign->dir.size - ign->dir_root; + + if (pathlen == dirlen && !memcmp(relpath, start, dirlen)) { + git_vector_pop(&ign->ign_path); + git_attr_file__free(file); + } + } + } + + if (--ign->depth > 0) { + git_str_rtruncate_at_char(&ign->dir, '/'); + git_fs_path_to_dir(&ign->dir); + } + + return 0; +} + +void git_ignore__free(git_ignores *ignores) +{ + unsigned int i; + git_attr_file *file; + + git_attr_file__free(ignores->ign_internal); + + git_vector_foreach(&ignores->ign_path, i, file) { + git_attr_file__free(file); + ignores->ign_path.contents[i] = NULL; + } + git_vector_free(&ignores->ign_path); + + git_vector_foreach(&ignores->ign_global, i, file) { + git_attr_file__free(file); + ignores->ign_global.contents[i] = NULL; + } + git_vector_free(&ignores->ign_global); + + git_str_dispose(&ignores->dir); +} + +static bool ignore_lookup_in_rules( + int *ignored, git_attr_file *file, git_attr_path *path) +{ + size_t j; + git_attr_fnmatch *match; + + git_vector_rforeach(&file->rules, j, match) { + if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY && + path->is_dir == GIT_DIR_FLAG_FALSE) + continue; + if (git_attr_fnmatch__match(match, path)) { + *ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ? + GIT_IGNORE_TRUE : GIT_IGNORE_FALSE; + return true; + } + } + + return false; +} + +int git_ignore__lookup( + int *out, git_ignores *ignores, const char *pathname, git_dir_flag dir_flag) +{ + size_t i; + git_attr_file *file; + git_attr_path path; + + *out = GIT_IGNORE_NOTFOUND; + + if (git_attr_path__init( + &path, pathname, git_repository_workdir(ignores->repo), dir_flag) < 0) + return -1; + + /* first process builtins - success means path was found */ + if (ignore_lookup_in_rules(out, ignores->ign_internal, &path)) + goto cleanup; + + /* next process files in the path. + * this process has to process ignores in reverse order + * to ensure correct prioritization of rules + */ + git_vector_rforeach(&ignores->ign_path, i, file) { + if (ignore_lookup_in_rules(out, file, &path)) + goto cleanup; + } + + /* last process global ignores */ + git_vector_foreach(&ignores->ign_global, i, file) { + if (ignore_lookup_in_rules(out, file, &path)) + goto cleanup; + } + +cleanup: + git_attr_path__free(&path); + return 0; +} + +int git_ignore_add_rule(git_repository *repo, const char *rules) +{ + int error; + git_attr_file *ign_internal = NULL; + + if ((error = get_internal_ignores(&ign_internal, repo)) < 0) + return error; + + error = parse_ignore_file(repo, ign_internal, rules, false); + git_attr_file__free(ign_internal); + + return error; +} + +int git_ignore_clear_internal_rules(git_repository *repo) +{ + int error; + git_attr_file *ign_internal; + + if ((error = get_internal_ignores(&ign_internal, repo)) < 0) + return error; + + if (!(error = git_attr_file__clear_rules(ign_internal, true))) + error = parse_ignore_file( + repo, ign_internal, GIT_IGNORE_DEFAULT_RULES, false); + + git_attr_file__free(ign_internal); + return error; +} + +int git_ignore_path_is_ignored( + int *ignored, + git_repository *repo, + const char *pathname) +{ + int error; + const char *workdir; + git_attr_path path; + git_ignores ignores; + unsigned int i; + git_attr_file *file; + git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(ignored); + GIT_ASSERT_ARG(pathname); + + workdir = git_repository_workdir(repo); + + memset(&path, 0, sizeof(path)); + memset(&ignores, 0, sizeof(ignores)); + + if (!git__suffixcmp(pathname, "/")) + dir_flag = GIT_DIR_FLAG_TRUE; + else if (git_repository_is_bare(repo)) + dir_flag = GIT_DIR_FLAG_FALSE; + + if ((error = git_attr_path__init(&path, pathname, workdir, dir_flag)) < 0 || + (error = git_ignore__for_path(repo, path.path, &ignores)) < 0) + goto cleanup; + + while (1) { + /* first process builtins - success means path was found */ + if (ignore_lookup_in_rules(ignored, ignores.ign_internal, &path)) + goto cleanup; + + /* next process files in the path */ + git_vector_foreach(&ignores.ign_path, i, file) { + if (ignore_lookup_in_rules(ignored, file, &path)) + goto cleanup; + } + + /* last process global ignores */ + git_vector_foreach(&ignores.ign_global, i, file) { + if (ignore_lookup_in_rules(ignored, file, &path)) + goto cleanup; + } + + /* move up one directory */ + if (path.basename == path.path) + break; + path.basename[-1] = '\0'; + while (path.basename > path.path && *path.basename != '/') + path.basename--; + if (path.basename > path.path) + path.basename++; + path.is_dir = 1; + + if ((error = git_ignore__pop_dir(&ignores)) < 0) + break; + } + + *ignored = 0; + +cleanup: + git_attr_path__free(&path); + git_ignore__free(&ignores); + return error; +} + +int git_ignore__check_pathspec_for_exact_ignores( + git_repository *repo, + git_vector *vspec, + bool no_fnmatch) +{ + int error = 0; + size_t i; + git_attr_fnmatch *match; + int ignored; + git_str path = GIT_STR_INIT; + const char *filename; + git_index *idx; + + if ((error = git_repository__ensure_not_bare( + repo, "validate pathspec")) < 0 || + (error = git_repository_index(&idx, repo)) < 0) + return error; + + git_vector_foreach(vspec, i, match) { + /* skip wildcard matches (if they are being used) */ + if ((match->flags & GIT_ATTR_FNMATCH_HASWILD) != 0 && + !no_fnmatch) + continue; + + filename = match->pattern; + + /* if file is already in the index, it's fine */ + if (git_index_get_bypath(idx, filename, 0) != NULL) + continue; + + if ((error = git_repository_workdir_path(&path, repo, filename)) < 0) + break; + + /* is there a file on disk that matches this exactly? */ + if (!git_fs_path_isfile(path.ptr)) + continue; + + /* is that file ignored? */ + if ((error = git_ignore_path_is_ignored(&ignored, repo, filename)) < 0) + break; + + if (ignored) { + git_error_set(GIT_ERROR_INVALID, "pathspec contains ignored file '%s'", + filename); + error = GIT_EINVALIDSPEC; + break; + } + } + + git_index_free(idx); + git_str_dispose(&path); + + return error; +} diff --git a/src/libgit2/ignore.h b/src/libgit2/ignore.h new file mode 100644 index 0000000..aa5ca62 --- /dev/null +++ b/src/libgit2/ignore.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_ignore_h__ +#define INCLUDE_ignore_h__ + +#include "common.h" + +#include "repository.h" +#include "vector.h" +#include "attr_file.h" + +#define GIT_IGNORE_FILE ".gitignore" +#define GIT_IGNORE_FILE_INREPO "exclude" +#define GIT_IGNORE_FILE_XDG "ignore" + +/* The git_ignores structure maintains three sets of ignores: + * - internal ignores + * - per directory ignores + * - global ignores (at lower priority than the others) + * As you traverse from one directory to another, you can push and pop + * directories onto git_ignores list efficiently. + */ +typedef struct { + git_repository *repo; + git_str dir; /* current directory reflected in ign_path */ + git_attr_file *ign_internal; + git_vector ign_path; + git_vector ign_global; + size_t dir_root; /* offset in dir to repo root */ + int ignore_case; + int depth; +} git_ignores; + +extern int git_ignore__for_path( + git_repository *repo, const char *path, git_ignores *ign); + +extern int git_ignore__push_dir(git_ignores *ign, const char *dir); + +extern int git_ignore__pop_dir(git_ignores *ign); + +extern void git_ignore__free(git_ignores *ign); + +enum { + GIT_IGNORE_UNCHECKED = -2, + GIT_IGNORE_NOTFOUND = -1, + GIT_IGNORE_FALSE = 0, + GIT_IGNORE_TRUE = 1 +}; + +extern int git_ignore__lookup(int *out, git_ignores *ign, const char *path, git_dir_flag dir_flag); + +/* command line Git sometimes generates an error message if given a + * pathspec that contains an exact match to an ignored file (provided + * --force isn't also given). This makes it easy to check it that has + * happened. Returns GIT_EINVALIDSPEC if the pathspec contains ignored + * exact matches (that are not already present in the index). + */ +extern int git_ignore__check_pathspec_for_exact_ignores( + git_repository *repo, git_vector *pathspec, bool no_fnmatch); + +#endif diff --git a/src/libgit2/index.c b/src/libgit2/index.c new file mode 100644 index 0000000..ccb3823 --- /dev/null +++ b/src/libgit2/index.c @@ -0,0 +1,3972 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "index.h" + +#include + +#include "repository.h" +#include "tree.h" +#include "tree-cache.h" +#include "hash.h" +#include "iterator.h" +#include "pathspec.h" +#include "ignore.h" +#include "blob.h" +#include "idxmap.h" +#include "diff.h" +#include "varint.h" +#include "path.h" + +#include "git2/odb.h" +#include "git2/oid.h" +#include "git2/blob.h" +#include "git2/config.h" +#include "git2/sys/index.h" + +static int index_apply_to_wd_diff(git_index *index, int action, const git_strarray *paths, + unsigned int flags, + git_index_matched_path_cb cb, void *payload); + +static const size_t INDEX_HEADER_SIZE = 12; + +static const unsigned int INDEX_VERSION_NUMBER_DEFAULT = 2; +static const unsigned int INDEX_VERSION_NUMBER_LB = 2; +static const unsigned int INDEX_VERSION_NUMBER_EXT = 3; +static const unsigned int INDEX_VERSION_NUMBER_COMP = 4; +static const unsigned int INDEX_VERSION_NUMBER_UB = 4; + +static const unsigned int INDEX_HEADER_SIG = 0x44495243; +static const char INDEX_EXT_TREECACHE_SIG[] = {'T', 'R', 'E', 'E'}; +static const char INDEX_EXT_UNMERGED_SIG[] = {'R', 'E', 'U', 'C'}; +static const char INDEX_EXT_CONFLICT_NAME_SIG[] = {'N', 'A', 'M', 'E'}; + +#define INDEX_OWNER(idx) ((git_repository *)(GIT_REFCOUNT_OWNER(idx))) + +struct index_header { + uint32_t signature; + uint32_t version; + uint32_t entry_count; +}; + +struct index_extension { + char signature[4]; + uint32_t extension_size; +}; + +struct entry_time { + uint32_t seconds; + uint32_t nanoseconds; +}; + +struct entry_common { + struct entry_time ctime; + struct entry_time mtime; + uint32_t dev; + uint32_t ino; + uint32_t mode; + uint32_t uid; + uint32_t gid; + uint32_t file_size; +}; + +#define entry_short(oid_size) \ + struct { \ + struct entry_common common; \ + unsigned char oid[oid_size]; \ + uint16_t flags; \ + char path[1]; /* arbitrary length */ \ + } + +#define entry_long(oid_size) \ + struct { \ + struct entry_common common; \ + unsigned char oid[oid_size]; \ + uint16_t flags; \ + uint16_t flags_extended; \ + char path[1]; /* arbitrary length */ \ + } + +typedef entry_short(GIT_OID_SHA1_SIZE) index_entry_short_sha1; +typedef entry_long(GIT_OID_SHA1_SIZE) index_entry_long_sha1; + +#ifdef GIT_EXPERIMENTAL_SHA256 +typedef entry_short(GIT_OID_SHA256_SIZE) index_entry_short_sha256; +typedef entry_long(GIT_OID_SHA256_SIZE) index_entry_long_sha256; +#endif + +#undef entry_short +#undef entry_long + +struct entry_srch_key { + const char *path; + size_t pathlen; + int stage; +}; + +struct entry_internal { + git_index_entry entry; + size_t pathlen; + char path[GIT_FLEX_ARRAY]; +}; + +struct reuc_entry_internal { + git_index_reuc_entry entry; + size_t pathlen; + char path[GIT_FLEX_ARRAY]; +}; + +bool git_index__enforce_unsaved_safety = false; + +/* local declarations */ +static int read_extension(size_t *read_len, git_index *index, size_t checksum_size, const char *buffer, size_t buffer_size); +static int read_header(struct index_header *dest, const void *buffer); + +static int parse_index(git_index *index, const char *buffer, size_t buffer_size); +static bool is_index_extended(git_index *index); +static int write_index(unsigned char checksum[GIT_HASH_MAX_SIZE], size_t *checksum_size, git_index *index, git_filebuf *file); + +static void index_entry_free(git_index_entry *entry); +static void index_entry_reuc_free(git_index_reuc_entry *reuc); + +GIT_INLINE(int) index_map_set(git_idxmap *map, git_index_entry *e, bool ignore_case) +{ + if (ignore_case) + return git_idxmap_icase_set((git_idxmap_icase *) map, e, e); + else + return git_idxmap_set(map, e, e); +} + +GIT_INLINE(int) index_map_delete(git_idxmap *map, git_index_entry *e, bool ignore_case) +{ + if (ignore_case) + return git_idxmap_icase_delete((git_idxmap_icase *) map, e); + else + return git_idxmap_delete(map, e); +} + +GIT_INLINE(int) index_map_resize(git_idxmap *map, size_t count, bool ignore_case) +{ + if (ignore_case) + return git_idxmap_icase_resize((git_idxmap_icase *) map, count); + else + return git_idxmap_resize(map, count); +} + +int git_index_entry_srch(const void *key, const void *array_member) +{ + const struct entry_srch_key *srch_key = key; + const struct entry_internal *entry = array_member; + int cmp; + size_t len1, len2, len; + + len1 = srch_key->pathlen; + len2 = entry->pathlen; + len = len1 < len2 ? len1 : len2; + + cmp = memcmp(srch_key->path, entry->path, len); + if (cmp) + return cmp; + if (len1 < len2) + return -1; + if (len1 > len2) + return 1; + + if (srch_key->stage != GIT_INDEX_STAGE_ANY) + return srch_key->stage - GIT_INDEX_ENTRY_STAGE(&entry->entry); + + return 0; +} + +int git_index_entry_isrch(const void *key, const void *array_member) +{ + const struct entry_srch_key *srch_key = key; + const struct entry_internal *entry = array_member; + int cmp; + size_t len1, len2, len; + + len1 = srch_key->pathlen; + len2 = entry->pathlen; + len = len1 < len2 ? len1 : len2; + + cmp = strncasecmp(srch_key->path, entry->path, len); + + if (cmp) + return cmp; + if (len1 < len2) + return -1; + if (len1 > len2) + return 1; + + if (srch_key->stage != GIT_INDEX_STAGE_ANY) + return srch_key->stage - GIT_INDEX_ENTRY_STAGE(&entry->entry); + + return 0; +} + +static int index_entry_srch_path(const void *path, const void *array_member) +{ + const git_index_entry *entry = array_member; + + return strcmp((const char *)path, entry->path); +} + +static int index_entry_isrch_path(const void *path, const void *array_member) +{ + const git_index_entry *entry = array_member; + + return strcasecmp((const char *)path, entry->path); +} + +int git_index_entry_cmp(const void *a, const void *b) +{ + int diff; + const git_index_entry *entry_a = a; + const git_index_entry *entry_b = b; + + diff = strcmp(entry_a->path, entry_b->path); + + if (diff == 0) + diff = (GIT_INDEX_ENTRY_STAGE(entry_a) - GIT_INDEX_ENTRY_STAGE(entry_b)); + + return diff; +} + +int git_index_entry_icmp(const void *a, const void *b) +{ + int diff; + const git_index_entry *entry_a = a; + const git_index_entry *entry_b = b; + + diff = strcasecmp(entry_a->path, entry_b->path); + + if (diff == 0) + diff = (GIT_INDEX_ENTRY_STAGE(entry_a) - GIT_INDEX_ENTRY_STAGE(entry_b)); + + return diff; +} + +static int conflict_name_cmp(const void *a, const void *b) +{ + const git_index_name_entry *name_a = a; + const git_index_name_entry *name_b = b; + + if (name_a->ancestor && !name_b->ancestor) + return 1; + + if (!name_a->ancestor && name_b->ancestor) + return -1; + + if (name_a->ancestor) + return strcmp(name_a->ancestor, name_b->ancestor); + + if (!name_a->ours || !name_b->ours) + return 0; + + return strcmp(name_a->ours, name_b->ours); +} + +/** + * TODO: enable this when resolving case insensitive conflicts + */ +#if 0 +static int conflict_name_icmp(const void *a, const void *b) +{ + const git_index_name_entry *name_a = a; + const git_index_name_entry *name_b = b; + + if (name_a->ancestor && !name_b->ancestor) + return 1; + + if (!name_a->ancestor && name_b->ancestor) + return -1; + + if (name_a->ancestor) + return strcasecmp(name_a->ancestor, name_b->ancestor); + + if (!name_a->ours || !name_b->ours) + return 0; + + return strcasecmp(name_a->ours, name_b->ours); +} +#endif + +static int reuc_srch(const void *key, const void *array_member) +{ + const git_index_reuc_entry *reuc = array_member; + + return strcmp(key, reuc->path); +} + +static int reuc_isrch(const void *key, const void *array_member) +{ + const git_index_reuc_entry *reuc = array_member; + + return strcasecmp(key, reuc->path); +} + +static int reuc_cmp(const void *a, const void *b) +{ + const git_index_reuc_entry *info_a = a; + const git_index_reuc_entry *info_b = b; + + return strcmp(info_a->path, info_b->path); +} + +static int reuc_icmp(const void *a, const void *b) +{ + const git_index_reuc_entry *info_a = a; + const git_index_reuc_entry *info_b = b; + + return strcasecmp(info_a->path, info_b->path); +} + +static void index_entry_reuc_free(git_index_reuc_entry *reuc) +{ + git__free(reuc); +} + +static void index_entry_free(git_index_entry *entry) +{ + if (!entry) + return; + + memset(&entry->id, 0, sizeof(entry->id)); + git__free(entry); +} + +unsigned int git_index__create_mode(unsigned int mode) +{ + if (S_ISLNK(mode)) + return S_IFLNK; + + if (S_ISDIR(mode) || (mode & S_IFMT) == (S_IFLNK | S_IFDIR)) + return (S_IFLNK | S_IFDIR); + + return S_IFREG | GIT_PERMS_CANONICAL(mode); +} + +static unsigned int index_merge_mode( + git_index *index, git_index_entry *existing, unsigned int mode) +{ + if (index->no_symlinks && S_ISREG(mode) && + existing && S_ISLNK(existing->mode)) + return existing->mode; + + if (index->distrust_filemode && S_ISREG(mode)) + return (existing && S_ISREG(existing->mode)) ? + existing->mode : git_index__create_mode(0666); + + return git_index__create_mode(mode); +} + +GIT_INLINE(int) index_find_in_entries( + size_t *out, git_vector *entries, git_vector_cmp entry_srch, + const char *path, size_t path_len, int stage) +{ + struct entry_srch_key srch_key; + srch_key.path = path; + srch_key.pathlen = !path_len ? strlen(path) : path_len; + srch_key.stage = stage; + return git_vector_bsearch2(out, entries, entry_srch, &srch_key); +} + +GIT_INLINE(int) index_find( + size_t *out, git_index *index, + const char *path, size_t path_len, int stage) +{ + git_vector_sort(&index->entries); + + return index_find_in_entries( + out, &index->entries, index->entries_search, path, path_len, stage); +} + +void git_index__set_ignore_case(git_index *index, bool ignore_case) +{ + index->ignore_case = ignore_case; + + if (ignore_case) { + index->entries_cmp_path = git__strcasecmp_cb; + index->entries_search = git_index_entry_isrch; + index->entries_search_path = index_entry_isrch_path; + index->reuc_search = reuc_isrch; + } else { + index->entries_cmp_path = git__strcmp_cb; + index->entries_search = git_index_entry_srch; + index->entries_search_path = index_entry_srch_path; + index->reuc_search = reuc_srch; + } + + git_vector_set_cmp(&index->entries, + ignore_case ? git_index_entry_icmp : git_index_entry_cmp); + git_vector_sort(&index->entries); + + git_vector_set_cmp(&index->reuc, ignore_case ? reuc_icmp : reuc_cmp); + git_vector_sort(&index->reuc); +} + +int git_index__open( + git_index **index_out, + const char *index_path, + git_oid_t oid_type) +{ + git_index *index; + int error = -1; + + GIT_ASSERT_ARG(index_out); + + index = git__calloc(1, sizeof(git_index)); + GIT_ERROR_CHECK_ALLOC(index); + + index->oid_type = oid_type; + + if (git_pool_init(&index->tree_pool, 1) < 0) + goto fail; + + if (index_path != NULL) { + index->index_file_path = git__strdup(index_path); + if (!index->index_file_path) + goto fail; + + /* Check if index file is stored on disk already */ + if (git_fs_path_exists(index->index_file_path) == true) + index->on_disk = 1; + } + + if (git_vector_init(&index->entries, 32, git_index_entry_cmp) < 0 || + git_idxmap_new(&index->entries_map) < 0 || + git_vector_init(&index->names, 8, conflict_name_cmp) < 0 || + git_vector_init(&index->reuc, 8, reuc_cmp) < 0 || + git_vector_init(&index->deleted, 8, git_index_entry_cmp) < 0) + goto fail; + + index->entries_cmp_path = git__strcmp_cb; + index->entries_search = git_index_entry_srch; + index->entries_search_path = index_entry_srch_path; + index->reuc_search = reuc_srch; + index->version = INDEX_VERSION_NUMBER_DEFAULT; + + if (index_path != NULL && (error = git_index_read(index, true)) < 0) + goto fail; + + *index_out = index; + GIT_REFCOUNT_INC(index); + + return 0; + +fail: + git_pool_clear(&index->tree_pool); + git_index_free(index); + return error; +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_index_open(git_index **index_out, const char *index_path, git_oid_t oid_type) +{ + return git_index__open(index_out, index_path, oid_type); +} +#else +int git_index_open(git_index **index_out, const char *index_path) +{ + return git_index__open(index_out, index_path, GIT_OID_SHA1); +} +#endif + +int git_index__new(git_index **out, git_oid_t oid_type) +{ + return git_index__open(out, NULL, oid_type); +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_index_new(git_index **out, git_oid_t oid_type) +{ + return git_index__new(out, oid_type); +} +#else +int git_index_new(git_index **out) +{ + return git_index__new(out, GIT_OID_SHA1); +} +#endif + +static void index_free(git_index *index) +{ + /* index iterators increment the refcount of the index, so if we + * get here then there should be no outstanding iterators. + */ + if (git_atomic32_get(&index->readers)) + return; + + git_index_clear(index); + git_idxmap_free(index->entries_map); + git_vector_free(&index->entries); + git_vector_free(&index->names); + git_vector_free(&index->reuc); + git_vector_free(&index->deleted); + + git__free(index->index_file_path); + + git__memzero(index, sizeof(*index)); + git__free(index); +} + +void git_index_free(git_index *index) +{ + if (index == NULL) + return; + + GIT_REFCOUNT_DEC(index, index_free); +} + +/* call with locked index */ +static void index_free_deleted(git_index *index) +{ + int readers = (int)git_atomic32_get(&index->readers); + size_t i; + + if (readers > 0 || !index->deleted.length) + return; + + for (i = 0; i < index->deleted.length; ++i) { + git_index_entry *ie = git_atomic_swap(index->deleted.contents[i], NULL); + index_entry_free(ie); + } + + git_vector_clear(&index->deleted); +} + +/* call with locked index */ +static int index_remove_entry(git_index *index, size_t pos) +{ + int error = 0; + git_index_entry *entry = git_vector_get(&index->entries, pos); + + if (entry != NULL) { + git_tree_cache_invalidate_path(index->tree, entry->path); + index_map_delete(index->entries_map, entry, index->ignore_case); + } + + error = git_vector_remove(&index->entries, pos); + + if (!error) { + if (git_atomic32_get(&index->readers) > 0) { + error = git_vector_insert(&index->deleted, entry); + } else { + index_entry_free(entry); + } + + index->dirty = 1; + } + + return error; +} + +int git_index_clear(git_index *index) +{ + int error = 0; + + GIT_ASSERT_ARG(index); + + index->dirty = 1; + index->tree = NULL; + git_pool_clear(&index->tree_pool); + + git_idxmap_clear(index->entries_map); + while (!error && index->entries.length > 0) + error = index_remove_entry(index, index->entries.length - 1); + + if (error) + goto done; + + index_free_deleted(index); + + if ((error = git_index_name_clear(index)) < 0 || + (error = git_index_reuc_clear(index)) < 0) + goto done; + + git_futils_filestamp_set(&index->stamp, NULL); + +done: + return error; +} + +static int create_index_error(int error, const char *msg) +{ + git_error_set_str(GIT_ERROR_INDEX, msg); + return error; +} + +int git_index_set_caps(git_index *index, int caps) +{ + unsigned int old_ignore_case; + + GIT_ASSERT_ARG(index); + + old_ignore_case = index->ignore_case; + + if (caps == GIT_INDEX_CAPABILITY_FROM_OWNER) { + git_repository *repo = INDEX_OWNER(index); + int val; + + if (!repo) + return create_index_error( + -1, "cannot access repository to set index caps"); + + if (!git_repository__configmap_lookup(&val, repo, GIT_CONFIGMAP_IGNORECASE)) + index->ignore_case = (val != 0); + if (!git_repository__configmap_lookup(&val, repo, GIT_CONFIGMAP_FILEMODE)) + index->distrust_filemode = (val == 0); + if (!git_repository__configmap_lookup(&val, repo, GIT_CONFIGMAP_SYMLINKS)) + index->no_symlinks = (val == 0); + } + else { + index->ignore_case = ((caps & GIT_INDEX_CAPABILITY_IGNORE_CASE) != 0); + index->distrust_filemode = ((caps & GIT_INDEX_CAPABILITY_NO_FILEMODE) != 0); + index->no_symlinks = ((caps & GIT_INDEX_CAPABILITY_NO_SYMLINKS) != 0); + } + + if (old_ignore_case != index->ignore_case) { + git_index__set_ignore_case(index, (bool)index->ignore_case); + } + + return 0; +} + +int git_index_caps(const git_index *index) +{ + return ((index->ignore_case ? GIT_INDEX_CAPABILITY_IGNORE_CASE : 0) | + (index->distrust_filemode ? GIT_INDEX_CAPABILITY_NO_FILEMODE : 0) | + (index->no_symlinks ? GIT_INDEX_CAPABILITY_NO_SYMLINKS : 0)); +} + +#ifndef GIT_DEPRECATE_HARD +const git_oid *git_index_checksum(git_index *index) +{ + return (git_oid *)index->checksum; +} +#endif + +/** + * Returns 1 for changed, 0 for not changed and <0 for errors + */ +static int compare_checksum(git_index *index) +{ + int fd; + ssize_t bytes_read; + unsigned char checksum[GIT_HASH_MAX_SIZE]; + size_t checksum_size = git_oid_size(index->oid_type); + + if ((fd = p_open(index->index_file_path, O_RDONLY)) < 0) + return fd; + + if (p_lseek(fd, (0 - (ssize_t)checksum_size), SEEK_END) < 0) { + p_close(fd); + git_error_set(GIT_ERROR_OS, "failed to seek to end of file"); + return -1; + } + + bytes_read = p_read(fd, checksum, checksum_size); + p_close(fd); + + if (bytes_read < (ssize_t)checksum_size) + return -1; + + return !!memcmp(checksum, index->checksum, checksum_size); +} + +int git_index_read(git_index *index, int force) +{ + int error = 0, updated; + git_str buffer = GIT_STR_INIT; + git_futils_filestamp stamp = index->stamp; + + if (!index->index_file_path) + return create_index_error(-1, + "failed to read index: The index is in-memory only"); + + index->on_disk = git_fs_path_exists(index->index_file_path); + + if (!index->on_disk) { + if (force && (error = git_index_clear(index)) < 0) + return error; + + index->dirty = 0; + return 0; + } + + if ((updated = git_futils_filestamp_check(&stamp, index->index_file_path) < 0) || + ((updated = compare_checksum(index)) < 0)) { + git_error_set( + GIT_ERROR_INDEX, + "failed to read index: '%s' no longer exists", + index->index_file_path); + return updated; + } + + if (!updated && !force) + return 0; + + error = git_futils_readbuffer(&buffer, index->index_file_path); + if (error < 0) + return error; + + index->tree = NULL; + git_pool_clear(&index->tree_pool); + + error = git_index_clear(index); + + if (!error) + error = parse_index(index, buffer.ptr, buffer.size); + + if (!error) { + git_futils_filestamp_set(&index->stamp, &stamp); + index->dirty = 0; + } + + git_str_dispose(&buffer); + return error; +} + +int git_index_read_safely(git_index *index) +{ + if (git_index__enforce_unsaved_safety && index->dirty) { + git_error_set(GIT_ERROR_INDEX, + "the index has unsaved changes that would be overwritten by this operation"); + return GIT_EINDEXDIRTY; + } + + return git_index_read(index, false); +} + +static bool is_racy_entry(git_index *index, const git_index_entry *entry) +{ + /* Git special-cases submodules in the check */ + if (S_ISGITLINK(entry->mode)) + return false; + + return git_index_entry_newer_than_index(entry, index); +} + +/* + * Force the next diff to take a look at those entries which have the + * same timestamp as the current index. + */ +static int truncate_racily_clean(git_index *index) +{ + size_t i; + int error; + git_index_entry *entry; + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + git_diff *diff = NULL; + git_vector paths = GIT_VECTOR_INIT; + git_diff_delta *delta; + + /* Nothing to do if there's no repo to talk about */ + if (!INDEX_OWNER(index)) + return 0; + + /* If there's no workdir, we can't know where to even check */ + if (!git_repository_workdir(INDEX_OWNER(index))) + return 0; + + diff_opts.flags |= GIT_DIFF_INCLUDE_TYPECHANGE | GIT_DIFF_IGNORE_SUBMODULES | GIT_DIFF_DISABLE_PATHSPEC_MATCH; + git_vector_foreach(&index->entries, i, entry) { + if ((entry->flags_extended & GIT_INDEX_ENTRY_UPTODATE) == 0 && + is_racy_entry(index, entry)) + git_vector_insert(&paths, (char *)entry->path); + } + + if (paths.length == 0) + goto done; + + diff_opts.pathspec.count = paths.length; + diff_opts.pathspec.strings = (char **)paths.contents; + + if ((error = git_diff_index_to_workdir(&diff, INDEX_OWNER(index), index, &diff_opts)) < 0) + return error; + + git_vector_foreach(&diff->deltas, i, delta) { + entry = (git_index_entry *)git_index_get_bypath(index, delta->old_file.path, 0); + + /* Ensure that we have a stage 0 for this file (ie, it's not a + * conflict), otherwise smudging it is quite pointless. + */ + if (entry) { + entry->file_size = 0; + index->dirty = 1; + } + } + +done: + git_diff_free(diff); + git_vector_free(&paths); + return 0; +} + +unsigned git_index_version(git_index *index) +{ + GIT_ASSERT_ARG(index); + + return index->version; +} + +int git_index_set_version(git_index *index, unsigned int version) +{ + GIT_ASSERT_ARG(index); + + if (version < INDEX_VERSION_NUMBER_LB || + version > INDEX_VERSION_NUMBER_UB) { + git_error_set(GIT_ERROR_INDEX, "invalid version number"); + return -1; + } + + index->version = version; + + return 0; +} + +int git_index_write(git_index *index) +{ + git_indexwriter writer = GIT_INDEXWRITER_INIT; + int error; + + truncate_racily_clean(index); + + if ((error = git_indexwriter_init(&writer, index)) == 0 && + (error = git_indexwriter_commit(&writer)) == 0) + index->dirty = 0; + + git_indexwriter_cleanup(&writer); + + return error; +} + +const char *git_index_path(const git_index *index) +{ + GIT_ASSERT_ARG_WITH_RETVAL(index, NULL); + return index->index_file_path; +} + +int git_index_write_tree(git_oid *oid, git_index *index) +{ + git_repository *repo; + + GIT_ASSERT_ARG(oid); + GIT_ASSERT_ARG(index); + + repo = INDEX_OWNER(index); + + if (repo == NULL) + return create_index_error(-1, "Failed to write tree. " + "the index file is not backed up by an existing repository"); + + return git_tree__write_index(oid, index, repo); +} + +int git_index_write_tree_to( + git_oid *oid, git_index *index, git_repository *repo) +{ + GIT_ASSERT_ARG(oid); + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(repo); + + return git_tree__write_index(oid, index, repo); +} + +size_t git_index_entrycount(const git_index *index) +{ + GIT_ASSERT_ARG(index); + + return index->entries.length; +} + +const git_index_entry *git_index_get_byindex( + git_index *index, size_t n) +{ + GIT_ASSERT_ARG_WITH_RETVAL(index, NULL); + + git_vector_sort(&index->entries); + return git_vector_get(&index->entries, n); +} + +const git_index_entry *git_index_get_bypath( + git_index *index, const char *path, int stage) +{ + git_index_entry key = {{ 0 }}; + git_index_entry *value; + + GIT_ASSERT_ARG_WITH_RETVAL(index, NULL); + + key.path = path; + GIT_INDEX_ENTRY_STAGE_SET(&key, stage); + + if (index->ignore_case) + value = git_idxmap_icase_get((git_idxmap_icase *) index->entries_map, &key); + else + value = git_idxmap_get(index->entries_map, &key); + + if (!value) { + git_error_set(GIT_ERROR_INDEX, "index does not contain '%s'", path); + return NULL; + } + + return value; +} + +void git_index_entry__init_from_stat( + git_index_entry *entry, struct stat *st, bool trust_mode) +{ + entry->ctime.seconds = (int32_t)st->st_ctime; + entry->mtime.seconds = (int32_t)st->st_mtime; +#if defined(GIT_USE_NSEC) + entry->mtime.nanoseconds = st->st_mtime_nsec; + entry->ctime.nanoseconds = st->st_ctime_nsec; +#endif + entry->dev = st->st_rdev; + entry->ino = st->st_ino; + entry->mode = (!trust_mode && S_ISREG(st->st_mode)) ? + git_index__create_mode(0666) : git_index__create_mode(st->st_mode); + entry->uid = st->st_uid; + entry->gid = st->st_gid; + entry->file_size = (uint32_t)st->st_size; +} + +static void index_entry_adjust_namemask( + git_index_entry *entry, + size_t path_length) +{ + entry->flags &= ~GIT_INDEX_ENTRY_NAMEMASK; + + if (path_length < GIT_INDEX_ENTRY_NAMEMASK) + entry->flags |= path_length & GIT_INDEX_ENTRY_NAMEMASK; + else + entry->flags |= GIT_INDEX_ENTRY_NAMEMASK; +} + +/* When `from_workdir` is true, we will validate the paths to avoid placing + * paths that are invalid for the working directory on the current filesystem + * (eg, on Windows, we will disallow `GIT~1`, `AUX`, `COM1`, etc). This + * function will *always* prevent `.git` and directory traversal `../` from + * being added to the index. + */ +static int index_entry_create( + git_index_entry **out, + git_repository *repo, + const char *path, + struct stat *st, + bool from_workdir) +{ + size_t pathlen = strlen(path), alloclen; + struct entry_internal *entry; + unsigned int path_valid_flags = GIT_PATH_REJECT_INDEX_DEFAULTS; + uint16_t mode = 0; + + /* always reject placing `.git` in the index and directory traversal. + * when requested, disallow platform-specific filenames and upgrade to + * the platform-specific `.git` tests (eg, `git~1`, etc). + */ + if (from_workdir) + path_valid_flags |= GIT_PATH_REJECT_WORKDIR_DEFAULTS; + if (st) + mode = st->st_mode; + + if (!git_path_is_valid(repo, path, mode, path_valid_flags)) { + git_error_set(GIT_ERROR_INDEX, "invalid path: '%s'", path); + return -1; + } + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, sizeof(struct entry_internal), pathlen); + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, 1); + entry = git__calloc(1, alloclen); + GIT_ERROR_CHECK_ALLOC(entry); + + entry->pathlen = pathlen; + memcpy(entry->path, path, pathlen); + entry->entry.path = entry->path; + + *out = (git_index_entry *)entry; + return 0; +} + +static int index_entry_init( + git_index_entry **entry_out, + git_index *index, + const char *rel_path) +{ + int error = 0; + git_index_entry *entry = NULL; + git_str path = GIT_STR_INIT; + struct stat st; + git_oid oid; + git_repository *repo; + + if (INDEX_OWNER(index) == NULL) + return create_index_error(-1, + "could not initialize index entry. " + "Index is not backed up by an existing repository."); + + /* + * FIXME: this is duplicated with the work in + * git_blob__create_from_paths. It should accept an optional stat + * structure so we can pass in the one we have to do here. + */ + repo = INDEX_OWNER(index); + if (git_repository__ensure_not_bare(repo, "create blob from file") < 0) + return GIT_EBAREREPO; + + if (git_repository_workdir_path(&path, repo, rel_path) < 0) + return -1; + + error = git_fs_path_lstat(path.ptr, &st); + git_str_dispose(&path); + + if (error < 0) + return error; + + if (index_entry_create(&entry, INDEX_OWNER(index), rel_path, &st, true) < 0) + return -1; + + /* write the blob to disk and get the oid and stat info */ + error = git_blob__create_from_paths( + &oid, &st, INDEX_OWNER(index), NULL, rel_path, 0, true); + + if (error < 0) { + index_entry_free(entry); + return error; + } + + entry->id = oid; + git_index_entry__init_from_stat(entry, &st, !index->distrust_filemode); + + *entry_out = (git_index_entry *)entry; + return 0; +} + +static git_index_reuc_entry *reuc_entry_alloc(const char *path) +{ + size_t pathlen = strlen(path), + structlen = sizeof(struct reuc_entry_internal), + alloclen; + struct reuc_entry_internal *entry; + + if (GIT_ADD_SIZET_OVERFLOW(&alloclen, structlen, pathlen) || + GIT_ADD_SIZET_OVERFLOW(&alloclen, alloclen, 1)) + return NULL; + + entry = git__calloc(1, alloclen); + if (!entry) + return NULL; + + entry->pathlen = pathlen; + memcpy(entry->path, path, pathlen); + entry->entry.path = entry->path; + + return (git_index_reuc_entry *)entry; +} + +static int index_entry_reuc_init(git_index_reuc_entry **reuc_out, + const char *path, + int ancestor_mode, const git_oid *ancestor_oid, + int our_mode, const git_oid *our_oid, + int their_mode, const git_oid *their_oid) +{ + git_index_reuc_entry *reuc = NULL; + + GIT_ASSERT_ARG(reuc_out); + GIT_ASSERT_ARG(path); + + *reuc_out = reuc = reuc_entry_alloc(path); + GIT_ERROR_CHECK_ALLOC(reuc); + + if ((reuc->mode[0] = ancestor_mode) > 0) { + GIT_ASSERT(ancestor_oid); + git_oid_cpy(&reuc->oid[0], ancestor_oid); + } + + if ((reuc->mode[1] = our_mode) > 0) { + GIT_ASSERT(our_oid); + git_oid_cpy(&reuc->oid[1], our_oid); + } + + if ((reuc->mode[2] = their_mode) > 0) { + GIT_ASSERT(their_oid); + git_oid_cpy(&reuc->oid[2], their_oid); + } + + return 0; +} + +static void index_entry_cpy( + git_index_entry *tgt, + const git_index_entry *src) +{ + const char *tgt_path = tgt->path; + memcpy(tgt, src, sizeof(*tgt)); + tgt->path = tgt_path; +} + +static int index_entry_dup( + git_index_entry **out, + git_index *index, + const git_index_entry *src) +{ + if (index_entry_create(out, INDEX_OWNER(index), src->path, NULL, false) < 0) + return -1; + + index_entry_cpy(*out, src); + return 0; +} + +static void index_entry_cpy_nocache( + git_index_entry *tgt, + const git_index_entry *src) +{ + git_oid_cpy(&tgt->id, &src->id); + tgt->mode = src->mode; + tgt->flags = src->flags; + tgt->flags_extended = (src->flags_extended & GIT_INDEX_ENTRY_EXTENDED_FLAGS); +} + +static int index_entry_dup_nocache( + git_index_entry **out, + git_index *index, + const git_index_entry *src) +{ + if (index_entry_create(out, INDEX_OWNER(index), src->path, NULL, false) < 0) + return -1; + + index_entry_cpy_nocache(*out, src); + return 0; +} + +static int has_file_name(git_index *index, + const git_index_entry *entry, size_t pos, int ok_to_replace) +{ + size_t len = strlen(entry->path); + int stage = GIT_INDEX_ENTRY_STAGE(entry); + const char *name = entry->path; + + while (pos < index->entries.length) { + struct entry_internal *p = index->entries.contents[pos++]; + + if (len >= p->pathlen) + break; + if (memcmp(name, p->path, len)) + break; + if (GIT_INDEX_ENTRY_STAGE(&p->entry) != stage) + continue; + if (p->path[len] != '/') + continue; + if (!ok_to_replace) + return -1; + + if (index_remove_entry(index, --pos) < 0) + break; + } + return 0; +} + +/* + * Do we have another file with a pathname that is a proper + * subset of the name we're trying to add? + */ +static int has_dir_name(git_index *index, + const git_index_entry *entry, int ok_to_replace) +{ + int stage = GIT_INDEX_ENTRY_STAGE(entry); + const char *name = entry->path; + const char *slash = name + strlen(name); + + for (;;) { + size_t len, pos; + + for (;;) { + slash--; + + if (slash <= entry->path) + return 0; + + if (*slash == '/') + break; + } + len = slash - name; + + if (!index_find(&pos, index, name, len, stage)) { + if (!ok_to_replace) + return -1; + + if (index_remove_entry(index, pos) < 0) + break; + continue; + } + + /* + * Trivial optimization: if we find an entry that + * already matches the sub-directory, then we know + * we're ok, and we can exit. + */ + for (; pos < index->entries.length; ++pos) { + struct entry_internal *p = index->entries.contents[pos]; + + if (p->pathlen <= len || + p->path[len] != '/' || + memcmp(p->path, name, len)) + break; /* not our subdirectory */ + + if (GIT_INDEX_ENTRY_STAGE(&p->entry) == stage) + return 0; + } + } + + return 0; +} + +static int check_file_directory_collision(git_index *index, + git_index_entry *entry, size_t pos, int ok_to_replace) +{ + if (has_file_name(index, entry, pos, ok_to_replace) < 0 || + has_dir_name(index, entry, ok_to_replace) < 0) { + git_error_set(GIT_ERROR_INDEX, + "'%s' appears as both a file and a directory", entry->path); + return -1; + } + + return 0; +} + +static int canonicalize_directory_path( + git_index *index, + git_index_entry *entry, + git_index_entry *existing) +{ + const git_index_entry *match, *best = NULL; + char *search, *sep; + size_t pos, search_len, best_len; + + if (!index->ignore_case) + return 0; + + /* item already exists in the index, simply re-use the existing case */ + if (existing) { + memcpy((char *)entry->path, existing->path, strlen(existing->path)); + return 0; + } + + /* nothing to do */ + if (strchr(entry->path, '/') == NULL) + return 0; + + if ((search = git__strdup(entry->path)) == NULL) + return -1; + + /* starting at the parent directory and descending to the root, find the + * common parent directory. + */ + while (!best && (sep = strrchr(search, '/'))) { + sep[1] = '\0'; + + search_len = strlen(search); + + git_vector_bsearch2( + &pos, &index->entries, index->entries_search_path, search); + + while ((match = git_vector_get(&index->entries, pos))) { + if (GIT_INDEX_ENTRY_STAGE(match) != 0) { + /* conflicts do not contribute to canonical paths */ + } else if (strncmp(search, match->path, search_len) == 0) { + /* prefer an exact match to the input filename */ + best = match; + best_len = search_len; + break; + } else if (strncasecmp(search, match->path, search_len) == 0) { + /* continue walking, there may be a path with an exact + * (case sensitive) match later in the index, but use this + * as the best match until that happens. + */ + if (!best) { + best = match; + best_len = search_len; + } + } else { + break; + } + + pos++; + } + + sep[0] = '\0'; + } + + if (best) + memcpy((char *)entry->path, best->path, best_len); + + git__free(search); + return 0; +} + +static int index_no_dups(void **old, void *new) +{ + const git_index_entry *entry = new; + GIT_UNUSED(old); + git_error_set(GIT_ERROR_INDEX, "'%s' appears multiple times at stage %d", + entry->path, GIT_INDEX_ENTRY_STAGE(entry)); + return GIT_EEXISTS; +} + +static void index_existing_and_best( + git_index_entry **existing, + size_t *existing_position, + git_index_entry **best, + git_index *index, + const git_index_entry *entry) +{ + git_index_entry *e; + size_t pos; + int error; + + error = index_find(&pos, + index, entry->path, 0, GIT_INDEX_ENTRY_STAGE(entry)); + + if (error == 0) { + *existing = index->entries.contents[pos]; + *existing_position = pos; + *best = index->entries.contents[pos]; + return; + } + + *existing = NULL; + *existing_position = 0; + *best = NULL; + + if (GIT_INDEX_ENTRY_STAGE(entry) == 0) { + for (; pos < index->entries.length; pos++) { + int (*strcomp)(const char *a, const char *b) = + index->ignore_case ? git__strcasecmp : git__strcmp; + + e = index->entries.contents[pos]; + + if (strcomp(entry->path, e->path) != 0) + break; + + if (GIT_INDEX_ENTRY_STAGE(e) == GIT_INDEX_STAGE_ANCESTOR) { + *best = e; + continue; + } else { + *best = e; + break; + } + } + } +} + +/* index_insert takes ownership of the new entry - if it can't insert + * it, then it will return an error **and also free the entry**. When + * it replaces an existing entry, it will update the entry_ptr with the + * actual entry in the index (and free the passed in one). + * + * trust_path is whether we use the given path, or whether (on case + * insensitive systems only) we try to canonicalize the given path to + * be within an existing directory. + * + * trust_mode is whether we trust the mode in entry_ptr. + * + * trust_id is whether we trust the id or it should be validated. + */ +static int index_insert( + git_index *index, + git_index_entry **entry_ptr, + int replace, + bool trust_path, + bool trust_mode, + bool trust_id) +{ + git_index_entry *existing, *best, *entry; + size_t path_length, position; + int error; + + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(entry_ptr); + + entry = *entry_ptr; + + /* Make sure that the path length flag is correct */ + path_length = ((struct entry_internal *)entry)->pathlen; + index_entry_adjust_namemask(entry, path_length); + + /* This entry is now up-to-date and should not be checked for raciness */ + entry->flags_extended |= GIT_INDEX_ENTRY_UPTODATE; + + git_vector_sort(&index->entries); + + /* + * Look if an entry with this path already exists, either staged, or (if + * this entry is a regular staged item) as the "ours" side of a conflict. + */ + index_existing_and_best(&existing, &position, &best, index, entry); + + /* Update the file mode */ + entry->mode = trust_mode ? + git_index__create_mode(entry->mode) : + index_merge_mode(index, best, entry->mode); + + /* Canonicalize the directory name */ + if (!trust_path && (error = canonicalize_directory_path(index, entry, best)) < 0) + goto out; + + /* Ensure that the given id exists (unless it's a submodule) */ + if (!trust_id && INDEX_OWNER(index) && + (entry->mode & GIT_FILEMODE_COMMIT) != GIT_FILEMODE_COMMIT) { + + if (!git_object__is_valid(INDEX_OWNER(index), &entry->id, + git_object__type_from_filemode(entry->mode))) { + error = -1; + goto out; + } + } + + /* Look for tree / blob name collisions, removing conflicts if requested */ + if ((error = check_file_directory_collision(index, entry, position, replace)) < 0) + goto out; + + /* + * If we are replacing an existing item, overwrite the existing entry + * and return it in place of the passed in one. + */ + if (existing) { + if (replace) { + index_entry_cpy(existing, entry); + + if (trust_path) + memcpy((char *)existing->path, entry->path, strlen(entry->path)); + } + + index_entry_free(entry); + *entry_ptr = existing; + } else { + /* + * If replace is not requested or no existing entry exists, insert + * at the sorted position. (Since we re-sort after each insert to + * check for dups, this is actually cheaper in the long run.) + */ + if ((error = git_vector_insert_sorted(&index->entries, entry, index_no_dups)) < 0 || + (error = index_map_set(index->entries_map, entry, index->ignore_case)) < 0) + goto out; + } + + index->dirty = 1; + +out: + if (error < 0) { + index_entry_free(*entry_ptr); + *entry_ptr = NULL; + } + + return error; +} + +static int index_conflict_to_reuc(git_index *index, const char *path) +{ + const git_index_entry *conflict_entries[3]; + int ancestor_mode, our_mode, their_mode; + git_oid const *ancestor_oid, *our_oid, *their_oid; + int ret; + + if ((ret = git_index_conflict_get(&conflict_entries[0], + &conflict_entries[1], &conflict_entries[2], index, path)) < 0) + return ret; + + ancestor_mode = conflict_entries[0] == NULL ? 0 : conflict_entries[0]->mode; + our_mode = conflict_entries[1] == NULL ? 0 : conflict_entries[1]->mode; + their_mode = conflict_entries[2] == NULL ? 0 : conflict_entries[2]->mode; + + ancestor_oid = conflict_entries[0] == NULL ? NULL : &conflict_entries[0]->id; + our_oid = conflict_entries[1] == NULL ? NULL : &conflict_entries[1]->id; + their_oid = conflict_entries[2] == NULL ? NULL : &conflict_entries[2]->id; + + if ((ret = git_index_reuc_add(index, path, ancestor_mode, ancestor_oid, + our_mode, our_oid, their_mode, their_oid)) >= 0) + ret = git_index_conflict_remove(index, path); + + return ret; +} + +GIT_INLINE(bool) is_file_or_link(const int filemode) +{ + return (filemode == GIT_FILEMODE_BLOB || + filemode == GIT_FILEMODE_BLOB_EXECUTABLE || + filemode == GIT_FILEMODE_LINK); +} + +GIT_INLINE(bool) valid_filemode(const int filemode) +{ + return (is_file_or_link(filemode) || filemode == GIT_FILEMODE_COMMIT); +} + +int git_index_add_from_buffer( + git_index *index, const git_index_entry *source_entry, + const void *buffer, size_t len) +{ + git_index_entry *entry = NULL; + int error = 0; + git_oid id; + + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(source_entry && source_entry->path); + + if (INDEX_OWNER(index) == NULL) + return create_index_error(-1, + "could not initialize index entry. " + "Index is not backed up by an existing repository."); + + if (!is_file_or_link(source_entry->mode)) { + git_error_set(GIT_ERROR_INDEX, "invalid filemode"); + return -1; + } + + if (len > UINT32_MAX) { + git_error_set(GIT_ERROR_INDEX, "buffer is too large"); + return -1; + } + + if (index_entry_dup(&entry, index, source_entry) < 0) + return -1; + + error = git_blob_create_from_buffer(&id, INDEX_OWNER(index), buffer, len); + if (error < 0) { + index_entry_free(entry); + return error; + } + + git_oid_cpy(&entry->id, &id); + entry->file_size = (uint32_t)len; + + if ((error = index_insert(index, &entry, 1, true, true, true)) < 0) + return error; + + /* Adding implies conflict was resolved, move conflict entries to REUC */ + if ((error = index_conflict_to_reuc(index, entry->path)) < 0 && error != GIT_ENOTFOUND) + return error; + + git_tree_cache_invalidate_path(index->tree, entry->path); + return 0; +} + +static int add_repo_as_submodule(git_index_entry **out, git_index *index, const char *path) +{ + git_repository *sub; + git_str abspath = GIT_STR_INIT; + git_repository *repo = INDEX_OWNER(index); + git_reference *head; + git_index_entry *entry; + struct stat st; + int error; + + if ((error = git_repository_workdir_path(&abspath, repo, path)) < 0) + return error; + + if ((error = p_stat(abspath.ptr, &st)) < 0) { + git_error_set(GIT_ERROR_OS, "failed to stat repository dir"); + return -1; + } + + if (index_entry_create(&entry, INDEX_OWNER(index), path, &st, true) < 0) + return -1; + + git_index_entry__init_from_stat(entry, &st, !index->distrust_filemode); + + if ((error = git_repository_open(&sub, abspath.ptr)) < 0) + return error; + + if ((error = git_repository_head(&head, sub)) < 0) + return error; + + git_oid_cpy(&entry->id, git_reference_target(head)); + entry->mode = GIT_FILEMODE_COMMIT; + + git_reference_free(head); + git_repository_free(sub); + git_str_dispose(&abspath); + + *out = entry; + return 0; +} + +int git_index_add_bypath(git_index *index, const char *path) +{ + git_index_entry *entry = NULL; + int ret; + + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(path); + + if ((ret = index_entry_init(&entry, index, path)) == 0) + ret = index_insert(index, &entry, 1, false, false, true); + + /* If we were given a directory, let's see if it's a submodule */ + if (ret < 0 && ret != GIT_EDIRECTORY) + return ret; + + if (ret == GIT_EDIRECTORY) { + git_submodule *sm; + git_error_state err; + + git_error_state_capture(&err, ret); + + ret = git_submodule_lookup(&sm, INDEX_OWNER(index), path); + if (ret == GIT_ENOTFOUND) + return git_error_state_restore(&err); + + git_error_state_free(&err); + + /* + * EEXISTS means that there is a repository at that path, but it's not known + * as a submodule. We add its HEAD as an entry and don't register it. + */ + if (ret == GIT_EEXISTS) { + if ((ret = add_repo_as_submodule(&entry, index, path)) < 0) + return ret; + + if ((ret = index_insert(index, &entry, 1, false, false, true)) < 0) + return ret; + } else if (ret < 0) { + return ret; + } else { + ret = git_submodule_add_to_index(sm, false); + git_submodule_free(sm); + return ret; + } + } + + /* Adding implies conflict was resolved, move conflict entries to REUC */ + if ((ret = index_conflict_to_reuc(index, path)) < 0 && ret != GIT_ENOTFOUND) + return ret; + + git_tree_cache_invalidate_path(index->tree, entry->path); + return 0; +} + +int git_index_remove_bypath(git_index *index, const char *path) +{ + int ret; + + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(path); + + if (((ret = git_index_remove(index, path, 0)) < 0 && + ret != GIT_ENOTFOUND) || + ((ret = index_conflict_to_reuc(index, path)) < 0 && + ret != GIT_ENOTFOUND)) + return ret; + + if (ret == GIT_ENOTFOUND) + git_error_clear(); + + return 0; +} + +int git_index__fill(git_index *index, const git_vector *source_entries) +{ + const git_index_entry *source_entry = NULL; + int error = 0; + size_t i; + + GIT_ASSERT_ARG(index); + + if (!source_entries->length) + return 0; + + if (git_vector_size_hint(&index->entries, source_entries->length) < 0 || + index_map_resize(index->entries_map, (size_t)(source_entries->length * 1.3), + index->ignore_case) < 0) + return -1; + + git_vector_foreach(source_entries, i, source_entry) { + git_index_entry *entry = NULL; + + if ((error = index_entry_dup(&entry, index, source_entry)) < 0) + break; + + index_entry_adjust_namemask(entry, ((struct entry_internal *)entry)->pathlen); + entry->flags_extended |= GIT_INDEX_ENTRY_UPTODATE; + entry->mode = git_index__create_mode(entry->mode); + + if ((error = git_vector_insert(&index->entries, entry)) < 0) + break; + + if ((error = index_map_set(index->entries_map, entry, index->ignore_case)) < 0) + break; + + index->dirty = 1; + } + + if (!error) + git_vector_sort(&index->entries); + + return error; +} + + +int git_index_add(git_index *index, const git_index_entry *source_entry) +{ + git_index_entry *entry = NULL; + int ret; + + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(source_entry && source_entry->path); + + if (!valid_filemode(source_entry->mode)) { + git_error_set(GIT_ERROR_INDEX, "invalid entry mode"); + return -1; + } + + if ((ret = index_entry_dup(&entry, index, source_entry)) < 0 || + (ret = index_insert(index, &entry, 1, true, true, false)) < 0) + return ret; + + git_tree_cache_invalidate_path(index->tree, entry->path); + return 0; +} + +int git_index_remove(git_index *index, const char *path, int stage) +{ + int error; + size_t position; + git_index_entry remove_key = {{ 0 }}; + + remove_key.path = path; + GIT_INDEX_ENTRY_STAGE_SET(&remove_key, stage); + + index_map_delete(index->entries_map, &remove_key, index->ignore_case); + + if (index_find(&position, index, path, 0, stage) < 0) { + git_error_set( + GIT_ERROR_INDEX, "index does not contain %s at stage %d", path, stage); + error = GIT_ENOTFOUND; + } else { + error = index_remove_entry(index, position); + } + + return error; +} + +int git_index_remove_directory(git_index *index, const char *dir, int stage) +{ + git_str pfx = GIT_STR_INIT; + int error = 0; + size_t pos; + git_index_entry *entry; + + if (!(error = git_str_sets(&pfx, dir)) && + !(error = git_fs_path_to_dir(&pfx))) + index_find(&pos, index, pfx.ptr, pfx.size, GIT_INDEX_STAGE_ANY); + + while (!error) { + entry = git_vector_get(&index->entries, pos); + if (!entry || git__prefixcmp(entry->path, pfx.ptr) != 0) + break; + + if (GIT_INDEX_ENTRY_STAGE(entry) != stage) { + ++pos; + continue; + } + + error = index_remove_entry(index, pos); + + /* removed entry at 'pos' so we don't need to increment */ + } + + git_str_dispose(&pfx); + + return error; +} + +int git_index_find_prefix(size_t *at_pos, git_index *index, const char *prefix) +{ + int error = 0; + size_t pos; + const git_index_entry *entry; + + index_find(&pos, index, prefix, strlen(prefix), GIT_INDEX_STAGE_ANY); + entry = git_vector_get(&index->entries, pos); + if (!entry || git__prefixcmp(entry->path, prefix) != 0) + error = GIT_ENOTFOUND; + + if (!error && at_pos) + *at_pos = pos; + + return error; +} + +int git_index__find_pos( + size_t *out, git_index *index, const char *path, size_t path_len, int stage) +{ + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(path); + return index_find(out, index, path, path_len, stage); +} + +int git_index_find(size_t *at_pos, git_index *index, const char *path) +{ + size_t pos; + + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(path); + + if (git_vector_bsearch2( + &pos, &index->entries, index->entries_search_path, path) < 0) { + git_error_set(GIT_ERROR_INDEX, "index does not contain %s", path); + return GIT_ENOTFOUND; + } + + /* Since our binary search only looked at path, we may be in the + * middle of a list of stages. + */ + for (; pos > 0; --pos) { + const git_index_entry *prev = git_vector_get(&index->entries, pos - 1); + + if (index->entries_cmp_path(prev->path, path) != 0) + break; + } + + if (at_pos) + *at_pos = pos; + + return 0; +} + +int git_index_conflict_add(git_index *index, + const git_index_entry *ancestor_entry, + const git_index_entry *our_entry, + const git_index_entry *their_entry) +{ + git_index_entry *entries[3] = { 0 }; + unsigned short i; + int ret = 0; + + GIT_ASSERT_ARG(index); + + if ((ancestor_entry && + (ret = index_entry_dup(&entries[0], index, ancestor_entry)) < 0) || + (our_entry && + (ret = index_entry_dup(&entries[1], index, our_entry)) < 0) || + (their_entry && + (ret = index_entry_dup(&entries[2], index, their_entry)) < 0)) + goto on_error; + + /* Validate entries */ + for (i = 0; i < 3; i++) { + if (entries[i] && !valid_filemode(entries[i]->mode)) { + git_error_set(GIT_ERROR_INDEX, "invalid filemode for stage %d entry", + i + 1); + ret = -1; + goto on_error; + } + } + + /* Remove existing index entries for each path */ + for (i = 0; i < 3; i++) { + if (entries[i] == NULL) + continue; + + if ((ret = git_index_remove(index, entries[i]->path, 0)) != 0) { + if (ret != GIT_ENOTFOUND) + goto on_error; + + git_error_clear(); + ret = 0; + } + } + + /* Add the conflict entries */ + for (i = 0; i < 3; i++) { + if (entries[i] == NULL) + continue; + + /* Make sure stage is correct */ + GIT_INDEX_ENTRY_STAGE_SET(entries[i], i + 1); + + if ((ret = index_insert(index, &entries[i], 1, true, true, false)) < 0) + goto on_error; + + entries[i] = NULL; /* don't free if later entry fails */ + } + + return 0; + +on_error: + for (i = 0; i < 3; i++) { + if (entries[i] != NULL) + index_entry_free(entries[i]); + } + + return ret; +} + +static int index_conflict__get_byindex( + const git_index_entry **ancestor_out, + const git_index_entry **our_out, + const git_index_entry **their_out, + git_index *index, + size_t n) +{ + const git_index_entry *conflict_entry; + const char *path = NULL; + size_t count; + int stage, len = 0; + + GIT_ASSERT_ARG(ancestor_out); + GIT_ASSERT_ARG(our_out); + GIT_ASSERT_ARG(their_out); + GIT_ASSERT_ARG(index); + + *ancestor_out = NULL; + *our_out = NULL; + *their_out = NULL; + + for (count = git_index_entrycount(index); n < count; ++n) { + conflict_entry = git_vector_get(&index->entries, n); + + if (path && index->entries_cmp_path(conflict_entry->path, path) != 0) + break; + + stage = GIT_INDEX_ENTRY_STAGE(conflict_entry); + path = conflict_entry->path; + + switch (stage) { + case 3: + *their_out = conflict_entry; + len++; + break; + case 2: + *our_out = conflict_entry; + len++; + break; + case 1: + *ancestor_out = conflict_entry; + len++; + break; + default: + break; + }; + } + + return len; +} + +int git_index_conflict_get( + const git_index_entry **ancestor_out, + const git_index_entry **our_out, + const git_index_entry **their_out, + git_index *index, + const char *path) +{ + size_t pos; + int len = 0; + + GIT_ASSERT_ARG(ancestor_out); + GIT_ASSERT_ARG(our_out); + GIT_ASSERT_ARG(their_out); + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(path); + + *ancestor_out = NULL; + *our_out = NULL; + *their_out = NULL; + + if (git_index_find(&pos, index, path) < 0) + return GIT_ENOTFOUND; + + if ((len = index_conflict__get_byindex( + ancestor_out, our_out, their_out, index, pos)) < 0) + return len; + else if (len == 0) + return GIT_ENOTFOUND; + + return 0; +} + +static int index_conflict_remove(git_index *index, const char *path) +{ + size_t pos = 0; + git_index_entry *conflict_entry; + int error = 0; + + if (path != NULL && git_index_find(&pos, index, path) < 0) + return GIT_ENOTFOUND; + + while ((conflict_entry = git_vector_get(&index->entries, pos)) != NULL) { + + if (path != NULL && + index->entries_cmp_path(conflict_entry->path, path) != 0) + break; + + if (GIT_INDEX_ENTRY_STAGE(conflict_entry) == 0) { + pos++; + continue; + } + + if ((error = index_remove_entry(index, pos)) < 0) + break; + } + + return error; +} + +int git_index_conflict_remove(git_index *index, const char *path) +{ + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(path); + return index_conflict_remove(index, path); +} + +int git_index_conflict_cleanup(git_index *index) +{ + GIT_ASSERT_ARG(index); + return index_conflict_remove(index, NULL); +} + +int git_index_has_conflicts(const git_index *index) +{ + size_t i; + git_index_entry *entry; + + GIT_ASSERT_ARG(index); + + git_vector_foreach(&index->entries, i, entry) { + if (GIT_INDEX_ENTRY_STAGE(entry) > 0) + return 1; + } + + return 0; +} + +int git_index_iterator_new( + git_index_iterator **iterator_out, + git_index *index) +{ + git_index_iterator *it; + int error; + + GIT_ASSERT_ARG(iterator_out); + GIT_ASSERT_ARG(index); + + it = git__calloc(1, sizeof(git_index_iterator)); + GIT_ERROR_CHECK_ALLOC(it); + + if ((error = git_index_snapshot_new(&it->snap, index)) < 0) { + git__free(it); + return error; + } + + it->index = index; + + *iterator_out = it; + return 0; +} + +int git_index_iterator_next( + const git_index_entry **out, + git_index_iterator *it) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(it); + + if (it->cur >= git_vector_length(&it->snap)) + return GIT_ITEROVER; + + *out = (git_index_entry *)git_vector_get(&it->snap, it->cur++); + return 0; +} + +void git_index_iterator_free(git_index_iterator *it) +{ + if (it == NULL) + return; + + git_index_snapshot_release(&it->snap, it->index); + git__free(it); +} + +int git_index_conflict_iterator_new( + git_index_conflict_iterator **iterator_out, + git_index *index) +{ + git_index_conflict_iterator *it = NULL; + + GIT_ASSERT_ARG(iterator_out); + GIT_ASSERT_ARG(index); + + it = git__calloc(1, sizeof(git_index_conflict_iterator)); + GIT_ERROR_CHECK_ALLOC(it); + + it->index = index; + + *iterator_out = it; + return 0; +} + +int git_index_conflict_next( + const git_index_entry **ancestor_out, + const git_index_entry **our_out, + const git_index_entry **their_out, + git_index_conflict_iterator *iterator) +{ + const git_index_entry *entry; + int len; + + GIT_ASSERT_ARG(ancestor_out); + GIT_ASSERT_ARG(our_out); + GIT_ASSERT_ARG(their_out); + GIT_ASSERT_ARG(iterator); + + *ancestor_out = NULL; + *our_out = NULL; + *their_out = NULL; + + while (iterator->cur < iterator->index->entries.length) { + entry = git_index_get_byindex(iterator->index, iterator->cur); + + if (git_index_entry_is_conflict(entry)) { + if ((len = index_conflict__get_byindex( + ancestor_out, + our_out, + their_out, + iterator->index, + iterator->cur)) < 0) + return len; + + iterator->cur += len; + return 0; + } + + iterator->cur++; + } + + return GIT_ITEROVER; +} + +void git_index_conflict_iterator_free(git_index_conflict_iterator *iterator) +{ + if (iterator == NULL) + return; + + git__free(iterator); +} + +size_t git_index_name_entrycount(git_index *index) +{ + GIT_ASSERT_ARG(index); + return index->names.length; +} + +const git_index_name_entry *git_index_name_get_byindex( + git_index *index, size_t n) +{ + GIT_ASSERT_ARG_WITH_RETVAL(index, NULL); + + git_vector_sort(&index->names); + return git_vector_get(&index->names, n); +} + +static void index_name_entry_free(git_index_name_entry *ne) +{ + if (!ne) + return; + git__free(ne->ancestor); + git__free(ne->ours); + git__free(ne->theirs); + git__free(ne); +} + +int git_index_name_add(git_index *index, + const char *ancestor, const char *ours, const char *theirs) +{ + git_index_name_entry *conflict_name; + + GIT_ASSERT_ARG((ancestor && ours) || (ancestor && theirs) || (ours && theirs)); + + conflict_name = git__calloc(1, sizeof(git_index_name_entry)); + GIT_ERROR_CHECK_ALLOC(conflict_name); + + if ((ancestor && !(conflict_name->ancestor = git__strdup(ancestor))) || + (ours && !(conflict_name->ours = git__strdup(ours))) || + (theirs && !(conflict_name->theirs = git__strdup(theirs))) || + git_vector_insert(&index->names, conflict_name) < 0) + { + index_name_entry_free(conflict_name); + return -1; + } + + index->dirty = 1; + return 0; +} + +int git_index_name_clear(git_index *index) +{ + size_t i; + git_index_name_entry *conflict_name; + + GIT_ASSERT_ARG(index); + + git_vector_foreach(&index->names, i, conflict_name) + index_name_entry_free(conflict_name); + + git_vector_clear(&index->names); + + index->dirty = 1; + + return 0; +} + +size_t git_index_reuc_entrycount(git_index *index) +{ + GIT_ASSERT_ARG(index); + return index->reuc.length; +} + +static int index_reuc_on_dup(void **old, void *new) +{ + index_entry_reuc_free(*old); + *old = new; + return GIT_EEXISTS; +} + +static int index_reuc_insert( + git_index *index, + git_index_reuc_entry *reuc) +{ + int res; + + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(reuc && reuc->path != NULL); + GIT_ASSERT(git_vector_is_sorted(&index->reuc)); + + res = git_vector_insert_sorted(&index->reuc, reuc, &index_reuc_on_dup); + index->dirty = 1; + + return res == GIT_EEXISTS ? 0 : res; +} + +int git_index_reuc_add(git_index *index, const char *path, + int ancestor_mode, const git_oid *ancestor_oid, + int our_mode, const git_oid *our_oid, + int their_mode, const git_oid *their_oid) +{ + git_index_reuc_entry *reuc = NULL; + int error = 0; + + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(path); + + if ((error = index_entry_reuc_init(&reuc, path, ancestor_mode, + ancestor_oid, our_mode, our_oid, their_mode, their_oid)) < 0 || + (error = index_reuc_insert(index, reuc)) < 0) + index_entry_reuc_free(reuc); + + return error; +} + +int git_index_reuc_find(size_t *at_pos, git_index *index, const char *path) +{ + return git_vector_bsearch2(at_pos, &index->reuc, index->reuc_search, path); +} + +const git_index_reuc_entry *git_index_reuc_get_bypath( + git_index *index, const char *path) +{ + size_t pos; + + GIT_ASSERT_ARG_WITH_RETVAL(index, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(path, NULL); + + if (!index->reuc.length) + return NULL; + + GIT_ASSERT_WITH_RETVAL(git_vector_is_sorted(&index->reuc), NULL); + + if (git_index_reuc_find(&pos, index, path) < 0) + return NULL; + + return git_vector_get(&index->reuc, pos); +} + +const git_index_reuc_entry *git_index_reuc_get_byindex( + git_index *index, size_t n) +{ + GIT_ASSERT_ARG_WITH_RETVAL(index, NULL); + GIT_ASSERT_WITH_RETVAL(git_vector_is_sorted(&index->reuc), NULL); + + return git_vector_get(&index->reuc, n); +} + +int git_index_reuc_remove(git_index *index, size_t position) +{ + int error; + git_index_reuc_entry *reuc; + + GIT_ASSERT_ARG(index); + GIT_ASSERT(git_vector_is_sorted(&index->reuc)); + + reuc = git_vector_get(&index->reuc, position); + error = git_vector_remove(&index->reuc, position); + + if (!error) + index_entry_reuc_free(reuc); + + index->dirty = 1; + return error; +} + +int git_index_reuc_clear(git_index *index) +{ + size_t i; + + GIT_ASSERT_ARG(index); + + for (i = 0; i < index->reuc.length; ++i) + index_entry_reuc_free(git_atomic_swap(index->reuc.contents[i], NULL)); + + git_vector_clear(&index->reuc); + + index->dirty = 1; + + return 0; +} + +static int index_error_invalid(const char *message) +{ + git_error_set(GIT_ERROR_INDEX, "invalid data in index - %s", message); + return -1; +} + +static int read_reuc(git_index *index, const char *buffer, size_t size) +{ + const char *endptr; + size_t oid_size = git_oid_size(index->oid_type); + size_t len; + int i; + + /* If called multiple times, the vector might already be initialized */ + if (index->reuc._alloc_size == 0 && + git_vector_init(&index->reuc, 16, reuc_cmp) < 0) + return -1; + + while (size) { + git_index_reuc_entry *lost; + + len = p_strnlen(buffer, size) + 1; + if (size <= len) + return index_error_invalid("reading reuc entries"); + + lost = reuc_entry_alloc(buffer); + GIT_ERROR_CHECK_ALLOC(lost); + + size -= len; + buffer += len; + + /* read 3 ASCII octal numbers for stage entries */ + for (i = 0; i < 3; i++) { + int64_t tmp; + + if (git__strntol64(&tmp, buffer, size, &endptr, 8) < 0 || + !endptr || endptr == buffer || *endptr || + tmp < 0 || tmp > UINT32_MAX) { + index_entry_reuc_free(lost); + return index_error_invalid("reading reuc entry stage"); + } + + lost->mode[i] = (uint32_t)tmp; + + len = (endptr + 1) - buffer; + if (size <= len) { + index_entry_reuc_free(lost); + return index_error_invalid("reading reuc entry stage"); + } + + size -= len; + buffer += len; + } + + /* read up to 3 OIDs for stage entries */ + for (i = 0; i < 3; i++) { + if (!lost->mode[i]) + continue; + if (size < oid_size) { + index_entry_reuc_free(lost); + return index_error_invalid("reading reuc entry oid"); + } + + if (git_oid__fromraw(&lost->oid[i], (const unsigned char *) buffer, index->oid_type) < 0) + return -1; + + size -= oid_size; + buffer += oid_size; + } + + /* entry was read successfully - insert into reuc vector */ + if (git_vector_insert(&index->reuc, lost) < 0) + return -1; + } + + /* entries are guaranteed to be sorted on-disk */ + git_vector_set_sorted(&index->reuc, true); + + return 0; +} + + +static int read_conflict_names(git_index *index, const char *buffer, size_t size) +{ + size_t len; + + /* This gets called multiple times, the vector might already be initialized */ + if (index->names._alloc_size == 0 && + git_vector_init(&index->names, 16, conflict_name_cmp) < 0) + return -1; + +#define read_conflict_name(ptr) \ + len = p_strnlen(buffer, size) + 1; \ + if (size < len) { \ + index_error_invalid("reading conflict name entries"); \ + goto out_err; \ + } \ + if (len == 1) \ + ptr = NULL; \ + else { \ + ptr = git__malloc(len); \ + GIT_ERROR_CHECK_ALLOC(ptr); \ + memcpy(ptr, buffer, len); \ + } \ + \ + buffer += len; \ + size -= len; + + while (size) { + git_index_name_entry *conflict_name = git__calloc(1, sizeof(git_index_name_entry)); + GIT_ERROR_CHECK_ALLOC(conflict_name); + + read_conflict_name(conflict_name->ancestor); + read_conflict_name(conflict_name->ours); + read_conflict_name(conflict_name->theirs); + + if (git_vector_insert(&index->names, conflict_name) < 0) + goto out_err; + + continue; + +out_err: + git__free(conflict_name->ancestor); + git__free(conflict_name->ours); + git__free(conflict_name->theirs); + git__free(conflict_name); + return -1; + } + +#undef read_conflict_name + + /* entries are guaranteed to be sorted on-disk */ + git_vector_set_sorted(&index->names, true); + + return 0; +} + +GIT_INLINE(size_t) index_entry_path_offset( + git_oid_t oid_type, + uint32_t flags) +{ + if (oid_type == GIT_OID_SHA1) + return (flags & GIT_INDEX_ENTRY_EXTENDED) ? + offsetof(index_entry_long_sha1, path) : + offsetof(index_entry_short_sha1, path); + +#ifdef GIT_EXPERIMENTAL_SHA256 + else if (oid_type == GIT_OID_SHA256) + return (flags & GIT_INDEX_ENTRY_EXTENDED) ? + offsetof(index_entry_long_sha256, path) : + offsetof(index_entry_short_sha256, path); +#endif + + git_error_set(GIT_ERROR_INTERNAL, "invalid oid type"); + return 0; +} + +GIT_INLINE(size_t) index_entry_flags_offset(git_oid_t oid_type) +{ + if (oid_type == GIT_OID_SHA1) + return offsetof(index_entry_long_sha1, flags_extended); + +#ifdef GIT_EXPERIMENTAL_SHA256 + else if (oid_type == GIT_OID_SHA256) + return offsetof(index_entry_long_sha256, flags_extended); +#endif + + git_error_set(GIT_ERROR_INTERNAL, "invalid oid type"); + return 0; +} + +static size_t index_entry_size( + size_t path_len, + size_t varint_len, + git_oid_t oid_type, + uint32_t flags) +{ + size_t offset, size; + + if (!(offset = index_entry_path_offset(oid_type, flags))) + return 0; + + if (varint_len) { + if (GIT_ADD_SIZET_OVERFLOW(&size, offset, path_len) || + GIT_ADD_SIZET_OVERFLOW(&size, size, 1) || + GIT_ADD_SIZET_OVERFLOW(&size, size, varint_len)) + return 0; + } else { + if (GIT_ADD_SIZET_OVERFLOW(&size, offset, path_len) || + GIT_ADD_SIZET_OVERFLOW(&size, size, 8)) + return 0; + + size &= ~7; + } + + return size; +} + +static int read_entry( + git_index_entry **out, + size_t *out_size, + git_index *index, + size_t checksum_size, + const void *buffer, + size_t buffer_size, + const char *last) +{ + size_t path_length, path_offset, entry_size; + const char *path_ptr; + struct entry_common *source_common; + index_entry_short_sha1 source_sha1; +#ifdef GIT_EXPERIMENTAL_SHA256 + index_entry_short_sha256 source_sha256; +#endif + git_index_entry entry = {{0}}; + bool compressed = index->version >= INDEX_VERSION_NUMBER_COMP; + char *tmp_path = NULL; + + size_t minimal_entry_size = index_entry_path_offset(index->oid_type, 0); + + if (checksum_size + minimal_entry_size > buffer_size) + return -1; + + /* buffer is not guaranteed to be aligned */ + switch (index->oid_type) { + case GIT_OID_SHA1: + source_common = &source_sha1.common; + memcpy(&source_sha1, buffer, sizeof(source_sha1)); + break; +#ifdef GIT_EXPERIMENTAL_SHA256 + case GIT_OID_SHA256: + source_common = &source_sha256.common; + memcpy(&source_sha256, buffer, sizeof(source_sha256)); + break; +#endif + default: + GIT_ASSERT(!"invalid oid type"); + } + + entry.ctime.seconds = (git_time_t)ntohl(source_common->ctime.seconds); + entry.ctime.nanoseconds = ntohl(source_common->ctime.nanoseconds); + entry.mtime.seconds = (git_time_t)ntohl(source_common->mtime.seconds); + entry.mtime.nanoseconds = ntohl(source_common->mtime.nanoseconds); + entry.dev = ntohl(source_common->dev); + entry.ino = ntohl(source_common->ino); + entry.mode = ntohl(source_common->mode); + entry.uid = ntohl(source_common->uid); + entry.gid = ntohl(source_common->gid); + entry.file_size = ntohl(source_common->file_size); + + switch (index->oid_type) { + case GIT_OID_SHA1: + if (git_oid__fromraw(&entry.id, source_sha1.oid, + GIT_OID_SHA1) < 0) + return -1; + entry.flags = ntohs(source_sha1.flags); + break; +#ifdef GIT_EXPERIMENTAL_SHA256 + case GIT_OID_SHA256: + if (git_oid__fromraw(&entry.id, source_sha256.oid, + GIT_OID_SHA256) < 0) + return -1; + entry.flags = ntohs(source_sha256.flags); + break; +#endif + default: + GIT_ASSERT(!"invalid oid type"); + } + + if (!(path_offset = index_entry_path_offset(index->oid_type, entry.flags))) + return -1; + + + if (entry.flags & GIT_INDEX_ENTRY_EXTENDED) { + uint16_t flags_raw; + size_t flags_offset; + + if (!(flags_offset = index_entry_flags_offset(index->oid_type))) + return -1; + + memcpy(&flags_raw, (const char *)buffer + flags_offset, sizeof(flags_raw)); + flags_raw = ntohs(flags_raw); + + memcpy(&entry.flags_extended, &flags_raw, sizeof(flags_raw)); + path_ptr = (const char *)buffer + path_offset; + } else { + path_ptr = (const char *)buffer + path_offset; + } + + if (!compressed) { + path_length = entry.flags & GIT_INDEX_ENTRY_NAMEMASK; + + /* if this is a very long string, we must find its + * real length without overflowing */ + if (path_length == 0xFFF) { + const char *path_end; + + path_end = memchr(path_ptr, '\0', buffer_size); + if (path_end == NULL) + return index_error_invalid("invalid path name"); + + path_length = path_end - path_ptr; + } + + entry_size = index_entry_size(path_length, 0, index->oid_type, entry.flags); + entry.path = (char *)path_ptr; + } else { + size_t varint_len, last_len, prefix_len, suffix_len, path_len; + uintmax_t strip_len; + + strip_len = git_decode_varint((const unsigned char *)path_ptr, &varint_len); + last_len = strlen(last); + + if (varint_len == 0 || last_len < strip_len) + return index_error_invalid("incorrect prefix length"); + + prefix_len = last_len - (size_t)strip_len; + suffix_len = strlen(path_ptr + varint_len); + + GIT_ERROR_CHECK_ALLOC_ADD(&path_len, prefix_len, suffix_len); + GIT_ERROR_CHECK_ALLOC_ADD(&path_len, path_len, 1); + + if (path_len > GIT_PATH_MAX) + return index_error_invalid("unreasonable path length"); + + tmp_path = git__malloc(path_len); + GIT_ERROR_CHECK_ALLOC(tmp_path); + + memcpy(tmp_path, last, prefix_len); + memcpy(tmp_path + prefix_len, path_ptr + varint_len, suffix_len + 1); + + entry_size = index_entry_size(suffix_len, varint_len, index->oid_type, entry.flags); + entry.path = tmp_path; + } + + if (entry_size == 0) + return -1; + + if (checksum_size + entry_size > buffer_size) { + git_error_set(GIT_ERROR_INTERNAL, "invalid index checksum"); + return -1; + } + + if (index_entry_dup(out, index, &entry) < 0) { + git__free(tmp_path); + return -1; + } + + git__free(tmp_path); + *out_size = entry_size; + return 0; +} + +static int read_header(struct index_header *dest, const void *buffer) +{ + const struct index_header *source = buffer; + + dest->signature = ntohl(source->signature); + if (dest->signature != INDEX_HEADER_SIG) + return index_error_invalid("incorrect header signature"); + + dest->version = ntohl(source->version); + if (dest->version < INDEX_VERSION_NUMBER_LB || + dest->version > INDEX_VERSION_NUMBER_UB) + return index_error_invalid("incorrect header version"); + + dest->entry_count = ntohl(source->entry_count); + return 0; +} + +static int read_extension(size_t *read_len, git_index *index, size_t checksum_size, const char *buffer, size_t buffer_size) +{ + struct index_extension dest; + size_t total_size; + + /* buffer is not guaranteed to be aligned */ + memcpy(&dest, buffer, sizeof(struct index_extension)); + dest.extension_size = ntohl(dest.extension_size); + + total_size = dest.extension_size + sizeof(struct index_extension); + + if (dest.extension_size > total_size || + buffer_size < total_size || + buffer_size - total_size < checksum_size) { + index_error_invalid("extension is truncated"); + return -1; + } + + /* optional extension */ + if (dest.signature[0] >= 'A' && dest.signature[0] <= 'Z') { + /* tree cache */ + if (memcmp(dest.signature, INDEX_EXT_TREECACHE_SIG, 4) == 0) { + if (git_tree_cache_read(&index->tree, buffer + 8, dest.extension_size, index->oid_type, &index->tree_pool) < 0) + return -1; + } else if (memcmp(dest.signature, INDEX_EXT_UNMERGED_SIG, 4) == 0) { + if (read_reuc(index, buffer + 8, dest.extension_size) < 0) + return -1; + } else if (memcmp(dest.signature, INDEX_EXT_CONFLICT_NAME_SIG, 4) == 0) { + if (read_conflict_names(index, buffer + 8, dest.extension_size) < 0) + return -1; + } + /* else, unsupported extension. We cannot parse this, but we can skip + * it by returning `total_size */ + } else { + /* we cannot handle non-ignorable extensions; + * in fact they aren't even defined in the standard */ + git_error_set(GIT_ERROR_INDEX, "unsupported mandatory extension: '%.4s'", dest.signature); + return -1; + } + + *read_len = total_size; + + return 0; +} + +static int parse_index(git_index *index, const char *buffer, size_t buffer_size) +{ + int error = 0; + unsigned int i; + struct index_header header = { 0 }; + unsigned char checksum[GIT_HASH_MAX_SIZE]; + size_t checksum_size = git_hash_size(git_oid_algorithm(index->oid_type)); + const char *last = NULL; + const char *empty = ""; + +#define seek_forward(_increase) { \ + if (_increase >= buffer_size) { \ + error = index_error_invalid("ran out of data while parsing"); \ + goto done; } \ + buffer += _increase; \ + buffer_size -= _increase;\ +} + + if (buffer_size < INDEX_HEADER_SIZE + checksum_size) + return index_error_invalid("insufficient buffer space"); + + /* + * Precalculate the hash of the files's contents -- we'll match + * it to the provided checksum in the footer. + */ + git_hash_buf(checksum, buffer, buffer_size - checksum_size, + git_oid_algorithm(index->oid_type)); + + /* Parse header */ + if ((error = read_header(&header, buffer)) < 0) + return error; + + index->version = header.version; + if (index->version >= INDEX_VERSION_NUMBER_COMP) + last = empty; + + seek_forward(INDEX_HEADER_SIZE); + + GIT_ASSERT(!index->entries.length); + + if ((error = index_map_resize(index->entries_map, header.entry_count, index->ignore_case)) < 0) + return error; + + /* Parse all the entries */ + for (i = 0; i < header.entry_count && buffer_size > checksum_size; ++i) { + git_index_entry *entry = NULL; + size_t entry_size; + + if ((error = read_entry(&entry, &entry_size, index, checksum_size, buffer, buffer_size, last)) < 0) { + error = index_error_invalid("invalid entry"); + goto done; + } + + if ((error = git_vector_insert(&index->entries, entry)) < 0) { + index_entry_free(entry); + goto done; + } + + if ((error = index_map_set(index->entries_map, entry, index->ignore_case)) < 0) { + index_entry_free(entry); + goto done; + } + error = 0; + + if (index->version >= INDEX_VERSION_NUMBER_COMP) + last = entry->path; + + seek_forward(entry_size); + } + + if (i != header.entry_count) { + error = index_error_invalid("header entries changed while parsing"); + goto done; + } + + /* There's still space for some extensions! */ + while (buffer_size > checksum_size) { + size_t extension_size; + + if ((error = read_extension(&extension_size, index, checksum_size, buffer, buffer_size)) < 0) { + goto done; + } + + seek_forward(extension_size); + } + + if (buffer_size != checksum_size) { + error = index_error_invalid( + "buffer size does not match index footer size"); + goto done; + } + + /* + * SHA-1 or SHA-256 (depending on the repository's object format) + * over the content of the index file before this checksum. + */ + if (memcmp(checksum, buffer, checksum_size) != 0) { + error = index_error_invalid( + "calculated checksum does not match expected"); + goto done; + } + + memcpy(index->checksum, checksum, checksum_size); + +#undef seek_forward + + /* Entries are stored case-sensitively on disk, so re-sort now if + * in-memory index is supposed to be case-insensitive + */ + git_vector_set_sorted(&index->entries, !index->ignore_case); + git_vector_sort(&index->entries); + + index->dirty = 0; +done: + return error; +} + +static bool is_index_extended(git_index *index) +{ + size_t i, extended; + git_index_entry *entry; + + extended = 0; + + git_vector_foreach(&index->entries, i, entry) { + entry->flags &= ~GIT_INDEX_ENTRY_EXTENDED; + if (entry->flags_extended & GIT_INDEX_ENTRY_EXTENDED_FLAGS) { + extended++; + entry->flags |= GIT_INDEX_ENTRY_EXTENDED; + } + } + + return (extended > 0); +} + +static int write_disk_entry( + git_index *index, + git_filebuf *file, + git_index_entry *entry, + const char *last) +{ + void *mem = NULL; + struct entry_common *ondisk_common; + size_t path_len, path_offset, disk_size; + int varint_len = 0; + char *path; + const char *path_start = entry->path; + size_t same_len = 0; + + index_entry_short_sha1 ondisk_sha1; + index_entry_long_sha1 ondisk_ext_sha1; +#ifdef GIT_EXPERIMENTAL_SHA256 + index_entry_short_sha256 ondisk_sha256; + index_entry_long_sha256 ondisk_ext_sha256; +#endif + + switch (index->oid_type) { + case GIT_OID_SHA1: + ondisk_common = &ondisk_sha1.common; + break; +#ifdef GIT_EXPERIMENTAL_SHA256 + case GIT_OID_SHA256: + ondisk_common = &ondisk_sha256.common; + break; +#endif + default: + GIT_ASSERT(!"invalid oid type"); + } + + path_len = ((struct entry_internal *)entry)->pathlen; + + if (last) { + const char *last_c = last; + + while (*path_start == *last_c) { + if (!*path_start || !*last_c) + break; + ++path_start; + ++last_c; + ++same_len; + } + path_len -= same_len; + varint_len = git_encode_varint(NULL, 0, strlen(last) - same_len); + } + + disk_size = index_entry_size(path_len, varint_len, index->oid_type, entry->flags); + + if (!disk_size || git_filebuf_reserve(file, &mem, disk_size) < 0) + return -1; + + memset(mem, 0x0, disk_size); + + /** + * Yes, we have to truncate. + * + * The on-disk format for Index entries clearly defines + * the time and size fields to be 4 bytes each -- so even if + * we store these values with 8 bytes on-memory, they must + * be truncated to 4 bytes before writing to disk. + * + * In 2038 I will be either too dead or too rich to care about this + */ + ondisk_common->ctime.seconds = htonl((uint32_t)entry->ctime.seconds); + ondisk_common->mtime.seconds = htonl((uint32_t)entry->mtime.seconds); + ondisk_common->ctime.nanoseconds = htonl(entry->ctime.nanoseconds); + ondisk_common->mtime.nanoseconds = htonl(entry->mtime.nanoseconds); + ondisk_common->dev = htonl(entry->dev); + ondisk_common->ino = htonl(entry->ino); + ondisk_common->mode = htonl(entry->mode); + ondisk_common->uid = htonl(entry->uid); + ondisk_common->gid = htonl(entry->gid); + ondisk_common->file_size = htonl((uint32_t)entry->file_size); + + switch (index->oid_type) { + case GIT_OID_SHA1: + git_oid_raw_cpy(ondisk_sha1.oid, entry->id.id, GIT_OID_SHA1_SIZE); + ondisk_sha1.flags = htons(entry->flags); + break; +#ifdef GIT_EXPERIMENTAL_SHA256 + case GIT_OID_SHA256: + git_oid_raw_cpy(ondisk_sha256.oid, entry->id.id, GIT_OID_SHA256_SIZE); + ondisk_sha256.flags = htons(entry->flags); + break; +#endif + default: + GIT_ASSERT(!"invalid oid type"); + } + + path_offset = index_entry_path_offset(index->oid_type, entry->flags); + + if (entry->flags & GIT_INDEX_ENTRY_EXTENDED) { + struct entry_common *ondisk_ext; + uint16_t flags_extended = htons(entry->flags_extended & + GIT_INDEX_ENTRY_EXTENDED_FLAGS); + + switch (index->oid_type) { + case GIT_OID_SHA1: + memcpy(&ondisk_ext_sha1, &ondisk_sha1, + sizeof(index_entry_short_sha1)); + ondisk_ext_sha1.flags_extended = flags_extended; + ondisk_ext = &ondisk_ext_sha1.common; + break; +#ifdef GIT_EXPERIMENTAL_SHA256 + case GIT_OID_SHA256: + memcpy(&ondisk_ext_sha256, &ondisk_sha256, + sizeof(index_entry_short_sha256)); + ondisk_ext_sha256.flags_extended = flags_extended; + ondisk_ext = &ondisk_ext_sha256.common; + break; +#endif + default: + GIT_ASSERT(!"invalid oid type"); + } + + memcpy(mem, ondisk_ext, path_offset); + } else { + switch (index->oid_type) { + case GIT_OID_SHA1: + memcpy(mem, &ondisk_sha1, path_offset); + break; +#ifdef GIT_EXPERIMENTAL_SHA256 + case GIT_OID_SHA256: + memcpy(mem, &ondisk_sha256, path_offset); + break; +#endif + default: + GIT_ASSERT(!"invalid oid type"); + } + } + + path = (char *)mem + path_offset; + disk_size -= path_offset; + + if (last) { + varint_len = git_encode_varint((unsigned char *) path, + disk_size, strlen(last) - same_len); + GIT_ASSERT(varint_len > 0); + + path += varint_len; + disk_size -= varint_len; + + /* + * If using path compression, we are not allowed + * to have additional trailing NULs. + */ + GIT_ASSERT(disk_size == path_len + 1); + } else { + /* + * If no path compression is used, we do have + * NULs as padding. As such, simply assert that + * we have enough space left to write the path. + */ + GIT_ASSERT(disk_size > path_len); + } + + memcpy(path, path_start, path_len + 1); + + return 0; +} + +static int write_entries(git_index *index, git_filebuf *file) +{ + int error = 0; + size_t i; + git_vector case_sorted = GIT_VECTOR_INIT, *entries = NULL; + git_index_entry *entry; + const char *last = NULL; + + /* If index->entries is sorted case-insensitively, then we need + * to re-sort it case-sensitively before writing */ + if (index->ignore_case) { + if ((error = git_vector_dup(&case_sorted, &index->entries, git_index_entry_cmp)) < 0) + goto done; + + git_vector_sort(&case_sorted); + entries = &case_sorted; + } else { + entries = &index->entries; + } + + if (index->version >= INDEX_VERSION_NUMBER_COMP) + last = ""; + + git_vector_foreach(entries, i, entry) { + if ((error = write_disk_entry(index, file, entry, last)) < 0) + break; + if (index->version >= INDEX_VERSION_NUMBER_COMP) + last = entry->path; + } + +done: + git_vector_free(&case_sorted); + return error; +} + +static int write_extension(git_filebuf *file, struct index_extension *header, git_str *data) +{ + struct index_extension ondisk; + + memset(&ondisk, 0x0, sizeof(struct index_extension)); + memcpy(&ondisk, header, 4); + ondisk.extension_size = htonl(header->extension_size); + + git_filebuf_write(file, &ondisk, sizeof(struct index_extension)); + return git_filebuf_write(file, data->ptr, data->size); +} + +static int create_name_extension_data(git_str *name_buf, git_index_name_entry *conflict_name) +{ + int error = 0; + + if (conflict_name->ancestor == NULL) + error = git_str_put(name_buf, "\0", 1); + else + error = git_str_put(name_buf, conflict_name->ancestor, strlen(conflict_name->ancestor) + 1); + + if (error != 0) + goto on_error; + + if (conflict_name->ours == NULL) + error = git_str_put(name_buf, "\0", 1); + else + error = git_str_put(name_buf, conflict_name->ours, strlen(conflict_name->ours) + 1); + + if (error != 0) + goto on_error; + + if (conflict_name->theirs == NULL) + error = git_str_put(name_buf, "\0", 1); + else + error = git_str_put(name_buf, conflict_name->theirs, strlen(conflict_name->theirs) + 1); + +on_error: + return error; +} + +static int write_name_extension(git_index *index, git_filebuf *file) +{ + git_str name_buf = GIT_STR_INIT; + git_vector *out = &index->names; + git_index_name_entry *conflict_name; + struct index_extension extension; + size_t i; + int error = 0; + + git_vector_foreach(out, i, conflict_name) { + if ((error = create_name_extension_data(&name_buf, conflict_name)) < 0) + goto done; + } + + memset(&extension, 0x0, sizeof(struct index_extension)); + memcpy(&extension.signature, INDEX_EXT_CONFLICT_NAME_SIG, 4); + extension.extension_size = (uint32_t)name_buf.size; + + error = write_extension(file, &extension, &name_buf); + + git_str_dispose(&name_buf); + +done: + return error; +} + +static int create_reuc_extension_data(git_str *reuc_buf, git_index *index, git_index_reuc_entry *reuc) +{ + size_t oid_size = git_oid_size(index->oid_type); + int i; + int error = 0; + + if ((error = git_str_put(reuc_buf, reuc->path, strlen(reuc->path) + 1)) < 0) + return error; + + for (i = 0; i < 3; i++) { + if ((error = git_str_printf(reuc_buf, "%o", reuc->mode[i])) < 0 || + (error = git_str_put(reuc_buf, "\0", 1)) < 0) + return error; + } + + for (i = 0; i < 3; i++) { + if (reuc->mode[i] && (error = git_str_put(reuc_buf, (char *)&reuc->oid[i].id, oid_size)) < 0) + return error; + } + + return 0; +} + +static int write_reuc_extension(git_index *index, git_filebuf *file) +{ + git_str reuc_buf = GIT_STR_INIT; + git_vector *out = &index->reuc; + git_index_reuc_entry *reuc; + struct index_extension extension; + size_t i; + int error = 0; + + git_vector_foreach(out, i, reuc) { + if ((error = create_reuc_extension_data(&reuc_buf, index, reuc)) < 0) + goto done; + } + + memset(&extension, 0x0, sizeof(struct index_extension)); + memcpy(&extension.signature, INDEX_EXT_UNMERGED_SIG, 4); + extension.extension_size = (uint32_t)reuc_buf.size; + + error = write_extension(file, &extension, &reuc_buf); + + git_str_dispose(&reuc_buf); + +done: + return error; +} + +static int write_tree_extension(git_index *index, git_filebuf *file) +{ + struct index_extension extension; + git_str buf = GIT_STR_INIT; + int error; + + if (index->tree == NULL) + return 0; + + if ((error = git_tree_cache_write(&buf, index->tree)) < 0) + return error; + + memset(&extension, 0x0, sizeof(struct index_extension)); + memcpy(&extension.signature, INDEX_EXT_TREECACHE_SIG, 4); + extension.extension_size = (uint32_t)buf.size; + + error = write_extension(file, &extension, &buf); + + git_str_dispose(&buf); + + return error; +} + +static void clear_uptodate(git_index *index) +{ + git_index_entry *entry; + size_t i; + + git_vector_foreach(&index->entries, i, entry) + entry->flags_extended &= ~GIT_INDEX_ENTRY_UPTODATE; +} + +static int write_index( + unsigned char checksum[GIT_HASH_MAX_SIZE], + size_t *checksum_size, + git_index *index, + git_filebuf *file) +{ + struct index_header header; + bool is_extended; + uint32_t index_version_number; + + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(file); + + GIT_ASSERT(index->oid_type); + + *checksum_size = git_hash_size(git_oid_algorithm(index->oid_type)); + + if (index->version <= INDEX_VERSION_NUMBER_EXT) { + is_extended = is_index_extended(index); + index_version_number = is_extended ? INDEX_VERSION_NUMBER_EXT : INDEX_VERSION_NUMBER_LB; + } else { + index_version_number = index->version; + } + + header.signature = htonl(INDEX_HEADER_SIG); + header.version = htonl(index_version_number); + header.entry_count = htonl((uint32_t)index->entries.length); + + if (git_filebuf_write(file, &header, sizeof(struct index_header)) < 0) + return -1; + + if (write_entries(index, file) < 0) + return -1; + + /* write the tree cache extension */ + if (index->tree != NULL && write_tree_extension(index, file) < 0) + return -1; + + /* write the rename conflict extension */ + if (index->names.length > 0 && write_name_extension(index, file) < 0) + return -1; + + /* write the reuc extension */ + if (index->reuc.length > 0 && write_reuc_extension(index, file) < 0) + return -1; + + /* get out the hash for all the contents we've appended to the file */ + git_filebuf_hash(checksum, file); + + /* write it at the end of the file */ + if (git_filebuf_write(file, checksum, *checksum_size) < 0) + return -1; + + /* file entries are no longer up to date */ + clear_uptodate(index); + + return 0; +} + +int git_index_entry_stage(const git_index_entry *entry) +{ + return GIT_INDEX_ENTRY_STAGE(entry); +} + +int git_index_entry_is_conflict(const git_index_entry *entry) +{ + return (GIT_INDEX_ENTRY_STAGE(entry) > 0); +} + +typedef struct read_tree_data { + git_index *index; + git_vector *old_entries; + git_vector *new_entries; + git_vector_cmp entry_cmp; + git_tree_cache *tree; +} read_tree_data; + +static int read_tree_cb( + const char *root, const git_tree_entry *tentry, void *payload) +{ + read_tree_data *data = payload; + git_index_entry *entry = NULL, *old_entry; + git_str path = GIT_STR_INIT; + size_t pos; + + if (git_tree_entry__is_tree(tentry)) + return 0; + + if (git_str_joinpath(&path, root, tentry->filename) < 0) + return -1; + + if (index_entry_create(&entry, INDEX_OWNER(data->index), path.ptr, NULL, false) < 0) + return -1; + + entry->mode = tentry->attr; + git_oid_cpy(&entry->id, git_tree_entry_id(tentry)); + + /* look for corresponding old entry and copy data to new entry */ + if (data->old_entries != NULL && + !index_find_in_entries( + &pos, data->old_entries, data->entry_cmp, path.ptr, 0, 0) && + (old_entry = git_vector_get(data->old_entries, pos)) != NULL && + entry->mode == old_entry->mode && + git_oid_equal(&entry->id, &old_entry->id)) + { + index_entry_cpy(entry, old_entry); + entry->flags_extended = 0; + } + + index_entry_adjust_namemask(entry, path.size); + git_str_dispose(&path); + + if (git_vector_insert(data->new_entries, entry) < 0) { + index_entry_free(entry); + return -1; + } + + return 0; +} + +int git_index_read_tree(git_index *index, const git_tree *tree) +{ + int error = 0; + git_vector entries = GIT_VECTOR_INIT; + git_idxmap *entries_map; + read_tree_data data; + size_t i; + git_index_entry *e; + + if (git_idxmap_new(&entries_map) < 0) + return -1; + + git_vector_set_cmp(&entries, index->entries._cmp); /* match sort */ + + data.index = index; + data.old_entries = &index->entries; + data.new_entries = &entries; + data.entry_cmp = index->entries_search; + + index->tree = NULL; + git_pool_clear(&index->tree_pool); + + git_vector_sort(&index->entries); + + if ((error = git_tree_walk(tree, GIT_TREEWALK_POST, read_tree_cb, &data)) < 0) + goto cleanup; + + if ((error = index_map_resize(entries_map, entries.length, index->ignore_case)) < 0) + goto cleanup; + + git_vector_foreach(&entries, i, e) { + if ((error = index_map_set(entries_map, e, index->ignore_case)) < 0) { + git_error_set(GIT_ERROR_INDEX, "failed to insert entry into map"); + return error; + } + } + + error = 0; + + git_vector_sort(&entries); + + if ((error = git_index_clear(index)) < 0) { + /* well, this isn't good */; + } else { + git_vector_swap(&entries, &index->entries); + entries_map = git_atomic_swap(index->entries_map, entries_map); + } + + index->dirty = 1; + +cleanup: + git_vector_free(&entries); + git_idxmap_free(entries_map); + if (error < 0) + return error; + + error = git_tree_cache_read_tree(&index->tree, tree, index->oid_type, &index->tree_pool); + + return error; +} + +static int git_index_read_iterator( + git_index *index, + git_iterator *new_iterator, + size_t new_length_hint) +{ + git_vector new_entries = GIT_VECTOR_INIT, + remove_entries = GIT_VECTOR_INIT; + git_idxmap *new_entries_map = NULL; + git_iterator *index_iterator = NULL; + git_iterator_options opts = GIT_ITERATOR_OPTIONS_INIT; + const git_index_entry *old_entry, *new_entry; + git_index_entry *entry; + size_t i; + int error; + + GIT_ASSERT((new_iterator->flags & GIT_ITERATOR_DONT_IGNORE_CASE)); + + if ((error = git_vector_init(&new_entries, new_length_hint, index->entries._cmp)) < 0 || + (error = git_vector_init(&remove_entries, index->entries.length, NULL)) < 0 || + (error = git_idxmap_new(&new_entries_map)) < 0) + goto done; + + if (new_length_hint && (error = index_map_resize(new_entries_map, new_length_hint, + index->ignore_case)) < 0) + goto done; + + opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE | + GIT_ITERATOR_INCLUDE_CONFLICTS; + + if ((error = git_iterator_for_index(&index_iterator, + git_index_owner(index), index, &opts)) < 0 || + ((error = git_iterator_current(&old_entry, index_iterator)) < 0 && + error != GIT_ITEROVER) || + ((error = git_iterator_current(&new_entry, new_iterator)) < 0 && + error != GIT_ITEROVER)) + goto done; + + while (true) { + git_index_entry + *dup_entry = NULL, + *add_entry = NULL, + *remove_entry = NULL; + int diff; + + error = 0; + + if (old_entry && new_entry) + diff = git_index_entry_cmp(old_entry, new_entry); + else if (!old_entry && new_entry) + diff = 1; + else if (old_entry && !new_entry) + diff = -1; + else + break; + + if (diff < 0) { + remove_entry = (git_index_entry *)old_entry; + } else if (diff > 0) { + dup_entry = (git_index_entry *)new_entry; + } else { + /* Path and stage are equal, if the OID is equal, keep it to + * keep the stat cache data. + */ + if (git_oid_equal(&old_entry->id, &new_entry->id) && + old_entry->mode == new_entry->mode) { + add_entry = (git_index_entry *)old_entry; + } else { + dup_entry = (git_index_entry *)new_entry; + remove_entry = (git_index_entry *)old_entry; + } + } + + if (dup_entry) { + if ((error = index_entry_dup_nocache(&add_entry, index, dup_entry)) < 0) + goto done; + + index_entry_adjust_namemask(add_entry, + ((struct entry_internal *)add_entry)->pathlen); + } + + /* invalidate this path in the tree cache if this is new (to + * invalidate the parent trees) + */ + if (dup_entry && !remove_entry && index->tree) + git_tree_cache_invalidate_path(index->tree, dup_entry->path); + + if (add_entry) { + if ((error = git_vector_insert(&new_entries, add_entry)) == 0) + error = index_map_set(new_entries_map, add_entry, + index->ignore_case); + } + + if (remove_entry && error >= 0) + error = git_vector_insert(&remove_entries, remove_entry); + + if (error < 0) { + git_error_set(GIT_ERROR_INDEX, "failed to insert entry"); + goto done; + } + + if (diff <= 0) { + if ((error = git_iterator_advance(&old_entry, index_iterator)) < 0 && + error != GIT_ITEROVER) + goto done; + } + + if (diff >= 0) { + if ((error = git_iterator_advance(&new_entry, new_iterator)) < 0 && + error != GIT_ITEROVER) + goto done; + } + } + + if ((error = git_index_name_clear(index)) < 0 || + (error = git_index_reuc_clear(index)) < 0) + goto done; + + git_vector_swap(&new_entries, &index->entries); + new_entries_map = git_atomic_swap(index->entries_map, new_entries_map); + + git_vector_foreach(&remove_entries, i, entry) { + if (index->tree) + git_tree_cache_invalidate_path(index->tree, entry->path); + + index_entry_free(entry); + } + + clear_uptodate(index); + + index->dirty = 1; + error = 0; + +done: + git_idxmap_free(new_entries_map); + git_vector_free(&new_entries); + git_vector_free(&remove_entries); + git_iterator_free(index_iterator); + return error; +} + +int git_index_read_index( + git_index *index, + const git_index *new_index) +{ + git_iterator *new_iterator = NULL; + git_iterator_options opts = GIT_ITERATOR_OPTIONS_INIT; + int error; + + opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE | + GIT_ITERATOR_INCLUDE_CONFLICTS; + + if ((error = git_iterator_for_index(&new_iterator, + git_index_owner(new_index), (git_index *)new_index, &opts)) < 0 || + (error = git_index_read_iterator(index, new_iterator, + new_index->entries.length)) < 0) + goto done; + +done: + git_iterator_free(new_iterator); + return error; +} + +git_repository *git_index_owner(const git_index *index) +{ + return INDEX_OWNER(index); +} + +enum { + INDEX_ACTION_NONE = 0, + INDEX_ACTION_UPDATE = 1, + INDEX_ACTION_REMOVE = 2, + INDEX_ACTION_ADDALL = 3 +}; + +int git_index_add_all( + git_index *index, + const git_strarray *paths, + unsigned int flags, + git_index_matched_path_cb cb, + void *payload) +{ + int error; + git_repository *repo; + git_pathspec ps; + bool no_fnmatch = (flags & GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH) != 0; + + GIT_ASSERT_ARG(index); + + repo = INDEX_OWNER(index); + if ((error = git_repository__ensure_not_bare(repo, "index add all")) < 0) + return error; + + if ((error = git_pathspec__init(&ps, paths)) < 0) + return error; + + /* optionally check that pathspec doesn't mention any ignored files */ + if ((flags & GIT_INDEX_ADD_CHECK_PATHSPEC) != 0 && + (flags & GIT_INDEX_ADD_FORCE) == 0 && + (error = git_ignore__check_pathspec_for_exact_ignores( + repo, &ps.pathspec, no_fnmatch)) < 0) + goto cleanup; + + error = index_apply_to_wd_diff(index, INDEX_ACTION_ADDALL, paths, flags, cb, payload); + + if (error) + git_error_set_after_callback(error); + +cleanup: + git_pathspec__clear(&ps); + + return error; +} + +struct foreach_diff_data { + git_index *index; + const git_pathspec *pathspec; + unsigned int flags; + git_index_matched_path_cb cb; + void *payload; +}; + +static int apply_each_file(const git_diff_delta *delta, float progress, void *payload) +{ + struct foreach_diff_data *data = payload; + const char *match, *path; + int error = 0; + + GIT_UNUSED(progress); + + path = delta->old_file.path; + + /* We only want those which match the pathspecs */ + if (!git_pathspec__match( + &data->pathspec->pathspec, path, false, (bool)data->index->ignore_case, + &match, NULL)) + return 0; + + if (data->cb) + error = data->cb(path, match, data->payload); + + if (error > 0) /* skip this entry */ + return 0; + if (error < 0) /* actual error */ + return error; + + /* If the workdir item does not exist, remove it from the index. */ + if ((delta->new_file.flags & GIT_DIFF_FLAG_EXISTS) == 0) + error = git_index_remove_bypath(data->index, path); + else + error = git_index_add_bypath(data->index, delta->new_file.path); + + return error; +} + +static int index_apply_to_wd_diff(git_index *index, int action, const git_strarray *paths, + unsigned int flags, + git_index_matched_path_cb cb, void *payload) +{ + int error; + git_diff *diff; + git_pathspec ps; + git_repository *repo; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + struct foreach_diff_data data = { + index, + NULL, + flags, + cb, + payload, + }; + + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(action == INDEX_ACTION_UPDATE || action == INDEX_ACTION_ADDALL); + + repo = INDEX_OWNER(index); + + if (!repo) { + return create_index_error(-1, + "cannot run update; the index is not backed up by a repository."); + } + + /* + * We do the matching ourselves instead of passing the list to + * diff because we want to tell the callback which one + * matched, which we do not know if we ask diff to filter for us. + */ + if ((error = git_pathspec__init(&ps, paths)) < 0) + return error; + + opts.flags = GIT_DIFF_INCLUDE_TYPECHANGE; + if (action == INDEX_ACTION_ADDALL) { + opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED | + GIT_DIFF_RECURSE_UNTRACKED_DIRS; + + if (flags == GIT_INDEX_ADD_FORCE) + opts.flags |= GIT_DIFF_INCLUDE_IGNORED | + GIT_DIFF_RECURSE_IGNORED_DIRS; + } + + if ((error = git_diff_index_to_workdir(&diff, repo, index, &opts)) < 0) + goto cleanup; + + data.pathspec = &ps; + error = git_diff_foreach(diff, apply_each_file, NULL, NULL, NULL, &data); + git_diff_free(diff); + + if (error) /* make sure error is set if callback stopped iteration */ + git_error_set_after_callback(error); + +cleanup: + git_pathspec__clear(&ps); + return error; +} + +static int index_apply_to_all( + git_index *index, + int action, + const git_strarray *paths, + git_index_matched_path_cb cb, + void *payload) +{ + int error = 0; + size_t i; + git_pathspec ps; + const char *match; + git_str path = GIT_STR_INIT; + + GIT_ASSERT_ARG(index); + + if ((error = git_pathspec__init(&ps, paths)) < 0) + return error; + + git_vector_sort(&index->entries); + + for (i = 0; !error && i < index->entries.length; ++i) { + git_index_entry *entry = git_vector_get(&index->entries, i); + + /* check if path actually matches */ + if (!git_pathspec__match( + &ps.pathspec, entry->path, false, (bool)index->ignore_case, + &match, NULL)) + continue; + + /* issue notification callback if requested */ + if (cb && (error = cb(entry->path, match, payload)) != 0) { + if (error > 0) { /* return > 0 means skip this one */ + error = 0; + continue; + } + if (error < 0) /* return < 0 means abort */ + break; + } + + /* index manipulation may alter entry, so don't depend on it */ + if ((error = git_str_sets(&path, entry->path)) < 0) + break; + + switch (action) { + case INDEX_ACTION_NONE: + break; + case INDEX_ACTION_UPDATE: + error = git_index_add_bypath(index, path.ptr); + + if (error == GIT_ENOTFOUND) { + git_error_clear(); + + error = git_index_remove_bypath(index, path.ptr); + + if (!error) /* back up foreach if we removed this */ + i--; + } + break; + case INDEX_ACTION_REMOVE: + if (!(error = git_index_remove_bypath(index, path.ptr))) + i--; /* back up foreach if we removed this */ + break; + default: + git_error_set(GIT_ERROR_INVALID, "unknown index action %d", action); + error = -1; + break; + } + } + + git_str_dispose(&path); + git_pathspec__clear(&ps); + + return error; +} + +int git_index_remove_all( + git_index *index, + const git_strarray *pathspec, + git_index_matched_path_cb cb, + void *payload) +{ + int error = index_apply_to_all( + index, INDEX_ACTION_REMOVE, pathspec, cb, payload); + + if (error) /* make sure error is set if callback stopped iteration */ + git_error_set_after_callback(error); + + return error; +} + +int git_index_update_all( + git_index *index, + const git_strarray *pathspec, + git_index_matched_path_cb cb, + void *payload) +{ + int error = index_apply_to_wd_diff(index, INDEX_ACTION_UPDATE, pathspec, 0, cb, payload); + if (error) /* make sure error is set if callback stopped iteration */ + git_error_set_after_callback(error); + + return error; +} + +int git_index_snapshot_new(git_vector *snap, git_index *index) +{ + int error; + + GIT_REFCOUNT_INC(index); + + git_atomic32_inc(&index->readers); + git_vector_sort(&index->entries); + + error = git_vector_dup(snap, &index->entries, index->entries._cmp); + + if (error < 0) + git_index_snapshot_release(snap, index); + + return error; +} + +void git_index_snapshot_release(git_vector *snap, git_index *index) +{ + git_vector_free(snap); + + git_atomic32_dec(&index->readers); + + git_index_free(index); +} + +int git_index_snapshot_find( + size_t *out, git_vector *entries, git_vector_cmp entry_srch, + const char *path, size_t path_len, int stage) +{ + return index_find_in_entries(out, entries, entry_srch, path, path_len, stage); +} + +int git_indexwriter_init( + git_indexwriter *writer, + git_index *index) +{ + int filebuf_hash, error; + + GIT_REFCOUNT_INC(index); + + writer->index = index; + + filebuf_hash = git_filebuf_hash_flags(git_oid_algorithm(index->oid_type)); + GIT_ASSERT(filebuf_hash); + + if (!index->index_file_path) + return create_index_error(-1, + "failed to write index: The index is in-memory only"); + + if ((error = git_filebuf_open(&writer->file, + index->index_file_path, + git_filebuf_hash_flags(filebuf_hash), + GIT_INDEX_FILE_MODE)) < 0) { + if (error == GIT_ELOCKED) + git_error_set(GIT_ERROR_INDEX, "the index is locked; this might be due to a concurrent or crashed process"); + + return error; + } + + writer->should_write = 1; + + return 0; +} + +int git_indexwriter_init_for_operation( + git_indexwriter *writer, + git_repository *repo, + unsigned int *checkout_strategy) +{ + git_index *index; + int error; + + if ((error = git_repository_index__weakptr(&index, repo)) < 0 || + (error = git_indexwriter_init(writer, index)) < 0) + return error; + + writer->should_write = (*checkout_strategy & GIT_CHECKOUT_DONT_WRITE_INDEX) == 0; + *checkout_strategy |= GIT_CHECKOUT_DONT_WRITE_INDEX; + + return 0; +} + +int git_indexwriter_commit(git_indexwriter *writer) +{ + unsigned char checksum[GIT_HASH_MAX_SIZE]; + size_t checksum_size; + int error; + + if (!writer->should_write) + return 0; + + git_vector_sort(&writer->index->entries); + git_vector_sort(&writer->index->reuc); + + if ((error = write_index(checksum, &checksum_size, writer->index, &writer->file)) < 0) { + git_indexwriter_cleanup(writer); + return error; + } + + if ((error = git_filebuf_commit(&writer->file)) < 0) + return error; + + if ((error = git_futils_filestamp_check( + &writer->index->stamp, writer->index->index_file_path)) < 0) { + git_error_set(GIT_ERROR_OS, "could not read index timestamp"); + return -1; + } + + writer->index->dirty = 0; + writer->index->on_disk = 1; + memcpy(writer->index->checksum, checksum, checksum_size); + + git_index_free(writer->index); + writer->index = NULL; + + return 0; +} + +void git_indexwriter_cleanup(git_indexwriter *writer) +{ + git_filebuf_cleanup(&writer->file); + + git_index_free(writer->index); + writer->index = NULL; +} + +/* Deprecated functions */ + +#ifndef GIT_DEPRECATE_HARD +int git_index_add_frombuffer( + git_index *index, const git_index_entry *source_entry, + const void *buffer, size_t len) +{ + return git_index_add_from_buffer(index, source_entry, buffer, len); +} +#endif diff --git a/src/libgit2/index.h b/src/libgit2/index.h new file mode 100644 index 0000000..53c2997 --- /dev/null +++ b/src/libgit2/index.h @@ -0,0 +1,207 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_index_h__ +#define INCLUDE_index_h__ + +#include "common.h" + +#include "futils.h" +#include "filebuf.h" +#include "vector.h" +#include "idxmap.h" +#include "tree-cache.h" +#include "git2/odb.h" +#include "git2/index.h" + +#define GIT_INDEX_FILE "index" +#define GIT_INDEX_FILE_MODE 0666 + +extern bool git_index__enforce_unsaved_safety; + +struct git_index { + git_refcount rc; + + char *index_file_path; + git_futils_filestamp stamp; + unsigned char checksum[GIT_HASH_MAX_SIZE]; + + git_vector entries; + git_idxmap *entries_map; + + git_vector deleted; /* deleted entries if readers > 0 */ + git_atomic32 readers; /* number of active iterators */ + + git_oid_t oid_type; + + unsigned int on_disk:1; + unsigned int ignore_case:1; + unsigned int distrust_filemode:1; + unsigned int no_symlinks:1; + unsigned int dirty:1; /* whether we have unsaved changes */ + + git_tree_cache *tree; + git_pool tree_pool; + + git_vector names; + git_vector reuc; + + git_vector_cmp entries_cmp_path; + git_vector_cmp entries_search; + git_vector_cmp entries_search_path; + git_vector_cmp reuc_search; + + unsigned int version; +}; + +struct git_index_iterator { + git_index *index; + git_vector snap; + size_t cur; +}; + +struct git_index_conflict_iterator { + git_index *index; + size_t cur; +}; + +extern void git_index_entry__init_from_stat( + git_index_entry *entry, struct stat *st, bool trust_mode); + +/* Index entry comparison functions for array sorting */ +extern int git_index_entry_cmp(const void *a, const void *b); +extern int git_index_entry_icmp(const void *a, const void *b); + +/* Index entry search functions for search using a search spec */ +extern int git_index_entry_srch(const void *a, const void *b); +extern int git_index_entry_isrch(const void *a, const void *b); + +/* Index time handling functions */ +GIT_INLINE(bool) git_index_time_eq(const git_index_time *one, const git_index_time *two) +{ + if (one->seconds != two->seconds) + return false; + +#ifdef GIT_USE_NSEC + if (one->nanoseconds != two->nanoseconds) + return false; +#endif + + return true; +} + +/* + * Test if the given index time is newer than the given existing index entry. + * If the timestamps are exactly equivalent, then the given index time is + * considered "racily newer" than the existing index entry. + */ +GIT_INLINE(bool) git_index_entry_newer_than_index( + const git_index_entry *entry, git_index *index) +{ + /* If we never read the index, we can't have this race either */ + if (!index || index->stamp.mtime.tv_sec == 0) + return false; + + /* If the timestamp is the same or newer than the index, it's racy */ +#if defined(GIT_USE_NSEC) + if ((int32_t)index->stamp.mtime.tv_sec < entry->mtime.seconds) + return true; + else if ((int32_t)index->stamp.mtime.tv_sec > entry->mtime.seconds) + return false; + else + return (uint32_t)index->stamp.mtime.tv_nsec <= entry->mtime.nanoseconds; +#else + return ((int32_t)index->stamp.mtime.tv_sec) <= entry->mtime.seconds; +#endif +} + +/* Search index for `path`, returning GIT_ENOTFOUND if it does not exist + * (but not setting an error message). + * + * `at_pos` is set to the position where it is or would be inserted. + * Pass `path_len` as strlen of path or 0 to call strlen internally. + */ +extern int git_index__find_pos( + size_t *at_pos, git_index *index, const char *path, size_t path_len, int stage); + +extern int git_index__fill(git_index *index, const git_vector *source_entries); + +extern void git_index__set_ignore_case(git_index *index, bool ignore_case); + +extern unsigned int git_index__create_mode(unsigned int mode); + +GIT_INLINE(const git_futils_filestamp *) git_index__filestamp(git_index *index) +{ + return &index->stamp; +} + +GIT_INLINE(unsigned char *) git_index__checksum(git_index *index) +{ + return index->checksum; +} + +/* SHA256-aware internal functions */ + +extern int git_index__new( + git_index **index_out, + git_oid_t oid_type); + +extern int git_index__open( + git_index **index_out, + const char *index_path, + git_oid_t oid_type); + +/* Copy the current entries vector *and* increment the index refcount. + * Call `git_index__release_snapshot` when done. + */ +extern int git_index_snapshot_new(git_vector *snap, git_index *index); +extern void git_index_snapshot_release(git_vector *snap, git_index *index); + +/* Allow searching in a snapshot; entries must already be sorted! */ +extern int git_index_snapshot_find( + size_t *at_pos, git_vector *snap, git_vector_cmp entry_srch, + const char *path, size_t path_len, int stage); + +/* Replace an index with a new index */ +int git_index_read_index(git_index *index, const git_index *new_index); + +GIT_INLINE(int) git_index_is_dirty(git_index *index) +{ + return index->dirty; +} + +extern int git_index_read_safely(git_index *index); + +typedef struct { + git_index *index; + git_filebuf file; + unsigned int should_write:1; +} git_indexwriter; + +#define GIT_INDEXWRITER_INIT { NULL, GIT_FILEBUF_INIT } + +/* Lock the index for eventual writing. */ +extern int git_indexwriter_init(git_indexwriter *writer, git_index *index); + +/* Lock the index for eventual writing by a repository operation: a merge, + * revert, cherry-pick or a rebase. Note that the given checkout strategy + * will be updated for the operation's use so that checkout will not write + * the index. + */ +extern int git_indexwriter_init_for_operation( + git_indexwriter *writer, + git_repository *repo, + unsigned int *checkout_strategy); + +/* Write the index and unlock it. */ +extern int git_indexwriter_commit(git_indexwriter *writer); + +/* Cleanup an index writing session, unlocking the file (if it is still + * locked and freeing any data structures. + */ +extern void git_indexwriter_cleanup(git_indexwriter *writer); + +#endif diff --git a/src/libgit2/indexer.c b/src/libgit2/indexer.c new file mode 100644 index 0000000..e559a19 --- /dev/null +++ b/src/libgit2/indexer.c @@ -0,0 +1,1483 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "indexer.h" + +#include "git2/indexer.h" +#include "git2/object.h" + +#include "commit.h" +#include "tree.h" +#include "tag.h" +#include "pack.h" +#include "mwindow.h" +#include "posix.h" +#include "pack.h" +#include "filebuf.h" +#include "oid.h" +#include "oidarray.h" +#include "oidmap.h" +#include "zstream.h" +#include "object.h" + +size_t git_indexer__max_objects = UINT32_MAX; + +#define UINT31_MAX (0x7FFFFFFF) + +struct entry { + git_oid oid; + uint32_t crc; + uint32_t offset; + uint64_t offset_long; +}; + +struct git_indexer { + unsigned int parsed_header :1, + pack_committed :1, + have_stream :1, + have_delta :1, + do_fsync :1, + do_verify :1; + git_oid_t oid_type; + struct git_pack_header hdr; + struct git_pack_file *pack; + unsigned int mode; + off64_t off; + off64_t entry_start; + git_object_t entry_type; + git_str entry_data; + git_packfile_stream stream; + size_t nr_objects; + git_vector objects; + git_vector deltas; + unsigned int fanout[256]; + git_hash_ctx hash_ctx; + unsigned char checksum[GIT_HASH_MAX_SIZE]; + char name[(GIT_HASH_MAX_SIZE * 2) + 1]; + git_indexer_progress_cb progress_cb; + void *progress_payload; + char objbuf[8*1024]; + + /* OIDs referenced from pack objects. Used for verification. */ + git_oidmap *expected_oids; + + /* Needed to look up objects which we want to inject to fix a thin pack */ + git_odb *odb; + + /* Fields for calculating the packfile trailer (hash of everything before it) */ + char inbuf[GIT_HASH_MAX_SIZE]; + size_t inbuf_len; + git_hash_ctx trailer; +}; + +struct delta_info { + off64_t delta_off; +}; + +#ifndef GIT_DEPRECATE_HARD +const git_oid *git_indexer_hash(const git_indexer *idx) +{ + return (git_oid *)idx->checksum; +} +#endif + +const char *git_indexer_name(const git_indexer *idx) +{ + return idx->name; +} + +static int parse_header(struct git_pack_header *hdr, struct git_pack_file *pack) +{ + int error; + git_map map; + + if ((error = p_mmap(&map, sizeof(*hdr), GIT_PROT_READ, GIT_MAP_SHARED, pack->mwf.fd, 0)) < 0) + return error; + + memcpy(hdr, map.data, sizeof(*hdr)); + p_munmap(&map); + + /* Verify we recognize this pack file format. */ + if (hdr->hdr_signature != ntohl(PACK_SIGNATURE)) { + git_error_set(GIT_ERROR_INDEXER, "wrong pack signature"); + return -1; + } + + if (!pack_version_ok(hdr->hdr_version)) { + git_error_set(GIT_ERROR_INDEXER, "wrong pack version"); + return -1; + } + + return 0; +} + +static int objects_cmp(const void *a, const void *b) +{ + const struct entry *entrya = a; + const struct entry *entryb = b; + + return git_oid__cmp(&entrya->oid, &entryb->oid); +} + +int git_indexer_options_init(git_indexer_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_indexer_options, GIT_INDEXER_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_indexer_init_options(git_indexer_options *opts, unsigned int version) +{ + return git_indexer_options_init(opts, version); +} +#endif + +GIT_INLINE(git_hash_algorithm_t) indexer_hash_algorithm(git_indexer *idx) +{ + switch (idx->oid_type) { + case GIT_OID_SHA1: + return GIT_HASH_ALGORITHM_SHA1; +#ifdef GIT_EXPERIMENTAL_SHA256 + case GIT_OID_SHA256: + return GIT_HASH_ALGORITHM_SHA256; +#endif + } + + return GIT_HASH_ALGORITHM_NONE; +} + +static int indexer_new( + git_indexer **out, + const char *prefix, + git_oid_t oid_type, + unsigned int mode, + git_odb *odb, + git_indexer_options *in_opts) +{ + git_indexer_options opts = GIT_INDEXER_OPTIONS_INIT; + git_indexer *idx; + git_str path = GIT_STR_INIT, tmp_path = GIT_STR_INIT; + static const char suff[] = "/pack"; + git_hash_algorithm_t checksum_type; + int error, fd = -1; + + if (in_opts) + memcpy(&opts, in_opts, sizeof(opts)); + + idx = git__calloc(1, sizeof(git_indexer)); + GIT_ERROR_CHECK_ALLOC(idx); + idx->oid_type = oid_type; + idx->odb = odb; + idx->progress_cb = opts.progress_cb; + idx->progress_payload = opts.progress_cb_payload; + idx->mode = mode ? mode : GIT_PACK_FILE_MODE; + git_str_init(&idx->entry_data, 0); + + checksum_type = indexer_hash_algorithm(idx); + + if ((error = git_hash_ctx_init(&idx->hash_ctx, checksum_type)) < 0 || + (error = git_hash_ctx_init(&idx->trailer, checksum_type)) < 0 || + (error = git_oidmap_new(&idx->expected_oids)) < 0) + goto cleanup; + + idx->do_verify = opts.verify; + + if (git_repository__fsync_gitdir) + idx->do_fsync = 1; + + error = git_str_joinpath(&path, prefix, suff); + if (error < 0) + goto cleanup; + + fd = git_futils_mktmp(&tmp_path, git_str_cstr(&path), idx->mode); + git_str_dispose(&path); + if (fd < 0) + goto cleanup; + + error = git_packfile_alloc(&idx->pack, git_str_cstr(&tmp_path), oid_type); + git_str_dispose(&tmp_path); + + if (error < 0) + goto cleanup; + + idx->pack->mwf.fd = fd; + if ((error = git_mwindow_file_register(&idx->pack->mwf)) < 0) + goto cleanup; + + *out = idx; + return 0; + +cleanup: + if (fd != -1) + p_close(fd); + + if (git_str_len(&tmp_path) > 0) + p_unlink(git_str_cstr(&tmp_path)); + + if (idx->pack != NULL) + p_unlink(idx->pack->pack_name); + + git_str_dispose(&path); + git_str_dispose(&tmp_path); + git__free(idx); + return -1; +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_indexer_new( + git_indexer **out, + const char *prefix, + git_oid_t oid_type, + git_indexer_options *opts) +{ + return indexer_new( + out, + prefix, + oid_type, + opts ? opts->mode : 0, + opts ? opts->odb : NULL, + opts); +} +#else +int git_indexer_new( + git_indexer **out, + const char *prefix, + unsigned int mode, + git_odb *odb, + git_indexer_options *opts) +{ + return indexer_new(out, prefix, GIT_OID_SHA1, mode, odb, opts); +} +#endif + +void git_indexer__set_fsync(git_indexer *idx, int do_fsync) +{ + idx->do_fsync = !!do_fsync; +} + +/* Try to store the delta so we can try to resolve it later */ +static int store_delta(git_indexer *idx) +{ + struct delta_info *delta; + + delta = git__calloc(1, sizeof(struct delta_info)); + GIT_ERROR_CHECK_ALLOC(delta); + delta->delta_off = idx->entry_start; + + if (git_vector_insert(&idx->deltas, delta) < 0) + return -1; + + return 0; +} + +static int hash_header(git_hash_ctx *ctx, off64_t len, git_object_t type) +{ + char buffer[64]; + size_t hdrlen; + int error; + + if ((error = git_odb__format_object_header(&hdrlen, + buffer, sizeof(buffer), (size_t)len, type)) < 0) + return error; + + return git_hash_update(ctx, buffer, hdrlen); +} + +static int hash_object_stream(git_indexer*idx, git_packfile_stream *stream) +{ + ssize_t read; + + GIT_ASSERT_ARG(idx); + GIT_ASSERT_ARG(stream); + + do { + if ((read = git_packfile_stream_read(stream, idx->objbuf, sizeof(idx->objbuf))) < 0) + break; + + if (idx->do_verify) + git_str_put(&idx->entry_data, idx->objbuf, read); + + git_hash_update(&idx->hash_ctx, idx->objbuf, read); + } while (read > 0); + + if (read < 0) + return (int)read; + + return 0; +} + +/* In order to create the packfile stream, we need to skip over the delta base description */ +static int advance_delta_offset(git_indexer *idx, git_object_t type) +{ + git_mwindow *w = NULL; + + GIT_ASSERT_ARG(type == GIT_OBJECT_REF_DELTA || type == GIT_OBJECT_OFS_DELTA); + + if (type == GIT_OBJECT_REF_DELTA) { + idx->off += git_oid_size(idx->oid_type); + } else { + off64_t base_off; + int error = get_delta_base(&base_off, idx->pack, &w, &idx->off, type, idx->entry_start); + git_mwindow_close(&w); + if (error < 0) + return error; + } + + return 0; +} + +/* Read from the stream and discard any output */ +static int read_object_stream(git_indexer *idx, git_packfile_stream *stream) +{ + ssize_t read; + + GIT_ASSERT_ARG(stream); + + do { + read = git_packfile_stream_read(stream, idx->objbuf, sizeof(idx->objbuf)); + } while (read > 0); + + if (read < 0) + return (int)read; + + return 0; +} + +static int crc_object(uint32_t *crc_out, git_mwindow_file *mwf, off64_t start, off64_t size) +{ + void *ptr; + uint32_t crc; + unsigned int left, len; + git_mwindow *w = NULL; + + crc = crc32(0L, Z_NULL, 0); + while (size) { + ptr = git_mwindow_open(mwf, &w, start, (size_t)size, &left); + if (ptr == NULL) + return -1; + + len = min(left, (unsigned int)size); + crc = crc32(crc, ptr, len); + size -= len; + start += len; + git_mwindow_close(&w); + } + + *crc_out = htonl(crc); + return 0; +} + +static int add_expected_oid(git_indexer *idx, const git_oid *oid) +{ + /* + * If we know about that object because it is stored in our ODB or + * because we have already processed it as part of our pack file, we do + * not have to expect it. + */ + if ((!idx->odb || !git_odb_exists(idx->odb, oid)) && + !git_oidmap_exists(idx->pack->idx_cache, oid) && + !git_oidmap_exists(idx->expected_oids, oid)) { + git_oid *dup = git__malloc(sizeof(*oid)); + GIT_ERROR_CHECK_ALLOC(dup); + git_oid_cpy(dup, oid); + return git_oidmap_set(idx->expected_oids, dup, dup); + } + + return 0; +} + +static int check_object_connectivity(git_indexer *idx, const git_rawobj *obj) +{ + git_object *object; + git_oid *expected; + int error = 0; + + if (obj->type != GIT_OBJECT_BLOB && + obj->type != GIT_OBJECT_TREE && + obj->type != GIT_OBJECT_COMMIT && + obj->type != GIT_OBJECT_TAG) + return 0; + + if (git_object__from_raw(&object, obj->data, obj->len, obj->type, idx->oid_type) < 0) { + /* + * parse_raw returns EINVALID on invalid data; downgrade + * that to a normal -1 error code. + */ + error = -1; + goto out; + } + + if ((expected = git_oidmap_get(idx->expected_oids, &object->cached.oid)) != NULL) { + git_oidmap_delete(idx->expected_oids, &object->cached.oid); + git__free(expected); + } + + /* + * Check whether this is a known object. If so, we can just continue as + * we assume that the ODB has a complete graph. + */ + if (idx->odb && git_odb_exists(idx->odb, &object->cached.oid)) + return 0; + + switch (obj->type) { + case GIT_OBJECT_TREE: + { + git_tree *tree = (git_tree *) object; + git_tree_entry *entry; + size_t i; + + git_array_foreach(tree->entries, i, entry) + if (add_expected_oid(idx, &entry->oid) < 0) + goto out; + + break; + } + case GIT_OBJECT_COMMIT: + { + git_commit *commit = (git_commit *) object; + git_oid *parent_oid; + size_t i; + + git_array_foreach(commit->parent_ids, i, parent_oid) + if (add_expected_oid(idx, parent_oid) < 0) + goto out; + + if (add_expected_oid(idx, &commit->tree_id) < 0) + goto out; + + break; + } + case GIT_OBJECT_TAG: + { + git_tag *tag = (git_tag *) object; + + if (add_expected_oid(idx, &tag->target) < 0) + goto out; + + break; + } + case GIT_OBJECT_BLOB: + default: + break; + } + +out: + git_object_free(object); + + return error; +} + +static int store_object(git_indexer *idx) +{ + int i, error; + git_oid oid; + struct entry *entry; + off64_t entry_size; + struct git_pack_entry *pentry; + off64_t entry_start = idx->entry_start; + + entry = git__calloc(1, sizeof(*entry)); + GIT_ERROR_CHECK_ALLOC(entry); + + pentry = git__calloc(1, sizeof(struct git_pack_entry)); + GIT_ERROR_CHECK_ALLOC(pentry); + + if (git_hash_final(oid.id, &idx->hash_ctx)) { + git__free(pentry); + goto on_error; + } + +#ifdef GIT_EXPERIMENTAL_SHA256 + oid.type = idx->oid_type; +#endif + + entry_size = idx->off - entry_start; + if (entry_start > UINT31_MAX) { + entry->offset = UINT32_MAX; + entry->offset_long = entry_start; + } else { + entry->offset = (uint32_t)entry_start; + } + + if (idx->do_verify) { + git_rawobj rawobj = { + idx->entry_data.ptr, + idx->entry_data.size, + idx->entry_type + }; + + if ((error = check_object_connectivity(idx, &rawobj)) < 0) + goto on_error; + } + + git_oid_cpy(&pentry->id, &oid); + pentry->offset = entry_start; + + if (git_oidmap_exists(idx->pack->idx_cache, &pentry->id)) { + const char *idstr = git_oid_tostr_s(&pentry->id); + + if (!idstr) + git_error_set(GIT_ERROR_INDEXER, "failed to parse object id"); + else + git_error_set(GIT_ERROR_INDEXER, "duplicate object %s found in pack", idstr); + + git__free(pentry); + goto on_error; + } + + if ((error = git_oidmap_set(idx->pack->idx_cache, &pentry->id, pentry)) < 0) { + git__free(pentry); + git_error_set_oom(); + goto on_error; + } + + git_oid_cpy(&entry->oid, &oid); + + if (crc_object(&entry->crc, &idx->pack->mwf, entry_start, entry_size) < 0) + goto on_error; + + /* Add the object to the list */ + if (git_vector_insert(&idx->objects, entry) < 0) + goto on_error; + + for (i = oid.id[0]; i < 256; ++i) { + idx->fanout[i]++; + } + + return 0; + +on_error: + git__free(entry); + + return -1; +} + +GIT_INLINE(bool) has_entry(git_indexer *idx, git_oid *id) +{ + return git_oidmap_exists(idx->pack->idx_cache, id); +} + +static int save_entry(git_indexer *idx, struct entry *entry, struct git_pack_entry *pentry, off64_t entry_start) +{ + int i; + + if (entry_start > UINT31_MAX) { + entry->offset = UINT32_MAX; + entry->offset_long = entry_start; + } else { + entry->offset = (uint32_t)entry_start; + } + + pentry->offset = entry_start; + + if (git_oidmap_exists(idx->pack->idx_cache, &pentry->id) || + git_oidmap_set(idx->pack->idx_cache, &pentry->id, pentry) < 0) { + git_error_set(GIT_ERROR_INDEXER, "cannot insert object into pack"); + return -1; + } + + /* Add the object to the list */ + if (git_vector_insert(&idx->objects, entry) < 0) + return -1; + + for (i = entry->oid.id[0]; i < 256; ++i) { + idx->fanout[i]++; + } + + return 0; +} + +static int hash_and_save(git_indexer *idx, git_rawobj *obj, off64_t entry_start) +{ + git_oid oid; + size_t entry_size; + struct entry *entry; + struct git_pack_entry *pentry = NULL; + + entry = git__calloc(1, sizeof(*entry)); + GIT_ERROR_CHECK_ALLOC(entry); + + if (git_odb__hashobj(&oid, obj, idx->oid_type) < 0) { + git_error_set(GIT_ERROR_INDEXER, "failed to hash object"); + goto on_error; + } + + pentry = git__calloc(1, sizeof(struct git_pack_entry)); + GIT_ERROR_CHECK_ALLOC(pentry); + + git_oid_cpy(&pentry->id, &oid); + git_oid_cpy(&entry->oid, &oid); + entry->crc = crc32(0L, Z_NULL, 0); + + entry_size = (size_t)(idx->off - entry_start); + if (crc_object(&entry->crc, &idx->pack->mwf, entry_start, entry_size) < 0) + goto on_error; + + return save_entry(idx, entry, pentry, entry_start); + +on_error: + git__free(pentry); + git__free(entry); + git__free(obj->data); + return -1; +} + +static int do_progress_callback(git_indexer *idx, git_indexer_progress *stats) +{ + if (idx->progress_cb) + return git_error_set_after_callback_function( + idx->progress_cb(stats, idx->progress_payload), + "indexer progress"); + return 0; +} + +/* Hash everything but the checksum trailer */ +static void hash_partially(git_indexer *idx, const uint8_t *data, size_t size) +{ + size_t to_expell, to_keep; + size_t oid_size = git_oid_size(idx->oid_type); + + if (size == 0) + return; + + /* + * Easy case, dump the buffer and the data minus the trailing + * checksum (SHA1 or SHA256). + */ + if (size >= oid_size) { + git_hash_update(&idx->trailer, idx->inbuf, idx->inbuf_len); + git_hash_update(&idx->trailer, data, size - oid_size); + + data += size - oid_size; + memcpy(idx->inbuf, data, oid_size); + idx->inbuf_len = oid_size; + return; + } + + /* We can just append */ + if (idx->inbuf_len + size <= oid_size) { + memcpy(idx->inbuf + idx->inbuf_len, data, size); + idx->inbuf_len += size; + return; + } + + /* We need to partially drain the buffer and then append */ + to_keep = oid_size - size; + to_expell = idx->inbuf_len - to_keep; + + git_hash_update(&idx->trailer, idx->inbuf, to_expell); + + memmove(idx->inbuf, idx->inbuf + to_expell, to_keep); + memcpy(idx->inbuf + to_keep, data, size); + idx->inbuf_len += size - to_expell; +} + +#if defined(NO_MMAP) || !defined(GIT_WIN32) + +static int write_at(git_indexer *idx, const void *data, off64_t offset, size_t size) +{ + size_t remaining_size = size; + const char *ptr = (const char *)data; + + /* Handle data size larger that ssize_t */ + while (remaining_size > 0) { + ssize_t nb; + HANDLE_EINTR(nb, p_pwrite(idx->pack->mwf.fd, (void *)ptr, + remaining_size, offset)); + if (nb <= 0) + return -1; + + ptr += nb; + offset += nb; + remaining_size -= nb; + } + + return 0; +} + +static int append_to_pack(git_indexer *idx, const void *data, size_t size) +{ + if (write_at(idx, data, idx->pack->mwf.size, size) < 0) { + git_error_set(GIT_ERROR_OS, "cannot extend packfile '%s'", idx->pack->pack_name); + return -1; + } + + return 0; +} + +#else + +/* + * Windows may keep different views to a networked file for the mmap- and + * open-accessed versions of a file, so any writes done through + * `write(2)`/`pwrite(2)` may not be reflected on the data that `mmap(2)` is + * able to read. + */ + +static int write_at(git_indexer *idx, const void *data, off64_t offset, size_t size) +{ + git_file fd = idx->pack->mwf.fd; + size_t mmap_alignment; + size_t page_offset; + off64_t page_start; + unsigned char *map_data; + git_map map; + int error; + + GIT_ASSERT_ARG(data); + GIT_ASSERT_ARG(size); + + if ((error = git__mmap_alignment(&mmap_alignment)) < 0) + return error; + + /* the offset needs to be at the mmap boundary for the platform */ + page_offset = offset % mmap_alignment; + page_start = offset - page_offset; + + if ((error = p_mmap(&map, page_offset + size, GIT_PROT_WRITE, GIT_MAP_SHARED, fd, page_start)) < 0) + return error; + + map_data = (unsigned char *)map.data; + memcpy(map_data + page_offset, data, size); + p_munmap(&map); + + return 0; +} + +static int append_to_pack(git_indexer *idx, const void *data, size_t size) +{ + off64_t new_size; + size_t mmap_alignment; + size_t page_offset; + off64_t page_start; + off64_t current_size = idx->pack->mwf.size; + int error; + + if (!size) + return 0; + + if ((error = git__mmap_alignment(&mmap_alignment)) < 0) + return error; + + /* Write a single byte to force the file system to allocate space now or + * report an error, since we can't report errors when writing using mmap. + * Round the size up to the nearest page so that we only need to perform file + * I/O when we add a page, instead of whenever we write even a single byte. */ + new_size = current_size + size; + page_offset = new_size % mmap_alignment; + page_start = new_size - page_offset; + + if (p_pwrite(idx->pack->mwf.fd, data, 1, page_start + mmap_alignment - 1) < 0) { + git_error_set(GIT_ERROR_OS, "cannot extend packfile '%s'", idx->pack->pack_name); + return -1; + } + + return write_at(idx, data, idx->pack->mwf.size, size); +} + +#endif + +static int read_stream_object(git_indexer *idx, git_indexer_progress *stats) +{ + git_packfile_stream *stream = &idx->stream; + off64_t entry_start = idx->off; + size_t oid_size, entry_size; + git_object_t type; + git_mwindow *w = NULL; + int error; + + oid_size = git_oid_size(idx->oid_type); + + if (idx->pack->mwf.size <= idx->off + (long long)oid_size) + return GIT_EBUFS; + + if (!idx->have_stream) { + error = git_packfile_unpack_header(&entry_size, &type, idx->pack, &w, &idx->off); + if (error == GIT_EBUFS) { + idx->off = entry_start; + return error; + } + if (error < 0) + return error; + + git_mwindow_close(&w); + idx->entry_start = entry_start; + git_hash_init(&idx->hash_ctx); + git_str_clear(&idx->entry_data); + + if (type == GIT_OBJECT_REF_DELTA || type == GIT_OBJECT_OFS_DELTA) { + error = advance_delta_offset(idx, type); + if (error == GIT_EBUFS) { + idx->off = entry_start; + return error; + } + if (error < 0) + return error; + + idx->have_delta = 1; + } else { + idx->have_delta = 0; + + error = hash_header(&idx->hash_ctx, entry_size, type); + if (error < 0) + return error; + } + + idx->have_stream = 1; + idx->entry_type = type; + + error = git_packfile_stream_open(stream, idx->pack, idx->off); + if (error < 0) + return error; + } + + if (idx->have_delta) { + error = read_object_stream(idx, stream); + } else { + error = hash_object_stream(idx, stream); + } + + idx->off = stream->curpos; + if (error == GIT_EBUFS) + return error; + + /* We want to free the stream reasorces no matter what here */ + idx->have_stream = 0; + git_packfile_stream_dispose(stream); + + if (error < 0) + return error; + + if (idx->have_delta) { + error = store_delta(idx); + } else { + error = store_object(idx); + } + + if (error < 0) + return error; + + if (!idx->have_delta) { + stats->indexed_objects++; + } + stats->received_objects++; + + if ((error = do_progress_callback(idx, stats)) != 0) + return error; + + return 0; +} + +int git_indexer_append(git_indexer *idx, const void *data, size_t size, git_indexer_progress *stats) +{ + int error = -1; + struct git_pack_header *hdr = &idx->hdr; + git_mwindow_file *mwf = &idx->pack->mwf; + + GIT_ASSERT_ARG(idx); + GIT_ASSERT_ARG(data); + GIT_ASSERT_ARG(stats); + + if ((error = append_to_pack(idx, data, size)) < 0) + return error; + + hash_partially(idx, data, (int)size); + + /* Make sure we set the new size of the pack */ + idx->pack->mwf.size += size; + + if (!idx->parsed_header) { + unsigned int total_objects; + + if ((unsigned)idx->pack->mwf.size < sizeof(struct git_pack_header)) + return 0; + + if ((error = parse_header(&idx->hdr, idx->pack)) < 0) + return error; + + idx->parsed_header = 1; + idx->nr_objects = ntohl(hdr->hdr_entries); + idx->off = sizeof(struct git_pack_header); + + if (idx->nr_objects <= git_indexer__max_objects) { + total_objects = (unsigned int)idx->nr_objects; + } else { + git_error_set(GIT_ERROR_INDEXER, "too many objects"); + return -1; + } + + if (git_oidmap_new(&idx->pack->idx_cache) < 0) + return -1; + + idx->pack->has_cache = 1; + if (git_vector_init(&idx->objects, total_objects, objects_cmp) < 0) + return -1; + + if (git_vector_init(&idx->deltas, total_objects / 2, NULL) < 0) + return -1; + + stats->received_objects = 0; + stats->local_objects = 0; + stats->total_deltas = 0; + stats->indexed_deltas = 0; + stats->indexed_objects = 0; + stats->total_objects = total_objects; + + if ((error = do_progress_callback(idx, stats)) != 0) + return error; + } + + /* Now that we have data in the pack, let's try to parse it */ + + /* As the file grows any windows we try to use will be out of date */ + if ((error = git_mwindow_free_all(mwf)) < 0) + goto on_error; + + while (stats->indexed_objects < idx->nr_objects) { + if ((error = read_stream_object(idx, stats)) != 0) { + if (error == GIT_EBUFS) + break; + else + goto on_error; + } + } + + return 0; + +on_error: + git_mwindow_free_all(mwf); + return error; +} + +static int index_path(git_str *path, git_indexer *idx, const char *suffix) +{ + const char prefix[] = "pack-"; + size_t slash = (size_t)path->size; + + /* search backwards for '/' */ + while (slash > 0 && path->ptr[slash - 1] != '/') + slash--; + + if (git_str_grow(path, slash + 1 + strlen(prefix) + + git_oid_hexsize(idx->oid_type) + strlen(suffix) + 1) < 0) + return -1; + + git_str_truncate(path, slash); + git_str_puts(path, prefix); + git_str_puts(path, idx->name); + git_str_puts(path, suffix); + + return git_str_oom(path) ? -1 : 0; +} + +/** + * Rewind the packfile by the trailer, as we might need to fix the + * packfile by injecting objects at the tail and must overwrite it. + */ +static int seek_back_trailer(git_indexer *idx) +{ + idx->pack->mwf.size -= git_oid_size(idx->oid_type); + return git_mwindow_free_all(&idx->pack->mwf); +} + +static int inject_object(git_indexer *idx, git_oid *id) +{ + git_odb_object *obj = NULL; + struct entry *entry = NULL; + struct git_pack_entry *pentry = NULL; + unsigned char empty_checksum[GIT_HASH_MAX_SIZE] = {0}; + unsigned char hdr[64]; + git_str buf = GIT_STR_INIT; + off64_t entry_start; + const void *data; + size_t len, hdr_len; + size_t checksum_size; + int error; + + checksum_size = git_hash_size(indexer_hash_algorithm(idx)); + + if ((error = seek_back_trailer(idx)) < 0) + goto cleanup; + + entry_start = idx->pack->mwf.size; + + if ((error = git_odb_read(&obj, idx->odb, id)) < 0) { + git_error_set(GIT_ERROR_INDEXER, "missing delta bases"); + goto cleanup; + } + + data = git_odb_object_data(obj); + len = git_odb_object_size(obj); + + entry = git__calloc(1, sizeof(*entry)); + GIT_ERROR_CHECK_ALLOC(entry); + + entry->crc = crc32(0L, Z_NULL, 0); + + /* Write out the object header */ + if ((error = git_packfile__object_header(&hdr_len, hdr, len, git_odb_object_type(obj))) < 0 || + (error = append_to_pack(idx, hdr, hdr_len)) < 0) + goto cleanup; + + idx->pack->mwf.size += hdr_len; + entry->crc = crc32(entry->crc, hdr, (uInt)hdr_len); + + if ((error = git_zstream_deflatebuf(&buf, data, len)) < 0) + goto cleanup; + + /* And then the compressed object */ + if ((error = append_to_pack(idx, buf.ptr, buf.size)) < 0) + goto cleanup; + + idx->pack->mwf.size += buf.size; + entry->crc = htonl(crc32(entry->crc, (unsigned char *)buf.ptr, (uInt)buf.size)); + git_str_dispose(&buf); + + /* Write a fake trailer so the pack functions play ball */ + + if ((error = append_to_pack(idx, empty_checksum, checksum_size)) < 0) + goto cleanup; + + idx->pack->mwf.size += git_oid_size(idx->oid_type); + + pentry = git__calloc(1, sizeof(struct git_pack_entry)); + GIT_ERROR_CHECK_ALLOC(pentry); + + git_oid_cpy(&pentry->id, id); + git_oid_cpy(&entry->oid, id); + idx->off = entry_start + hdr_len + len; + + error = save_entry(idx, entry, pentry, entry_start); + +cleanup: + if (error) { + git__free(entry); + git__free(pentry); + } + + git_odb_object_free(obj); + return error; +} + +static int fix_thin_pack(git_indexer *idx, git_indexer_progress *stats) +{ + int error, found_ref_delta = 0; + unsigned int i; + struct delta_info *delta; + size_t size; + git_object_t type; + git_mwindow *w = NULL; + off64_t curpos = 0; + unsigned char *base_info; + unsigned int left = 0; + git_oid base; + + GIT_ASSERT(git_vector_length(&idx->deltas) > 0); + + if (idx->odb == NULL) { + git_error_set(GIT_ERROR_INDEXER, "cannot fix a thin pack without an ODB"); + return -1; + } + + /* Loop until we find the first REF delta */ + git_vector_foreach(&idx->deltas, i, delta) { + if (!delta) + continue; + + curpos = delta->delta_off; + error = git_packfile_unpack_header(&size, &type, idx->pack, &w, &curpos); + if (error < 0) + return error; + + if (type == GIT_OBJECT_REF_DELTA) { + found_ref_delta = 1; + break; + } + } + + if (!found_ref_delta) { + git_error_set(GIT_ERROR_INDEXER, "no REF_DELTA found, cannot inject object"); + return -1; + } + + /* curpos now points to the base information, which is an OID */ + base_info = git_mwindow_open(&idx->pack->mwf, &w, curpos, git_oid_size(idx->oid_type), &left); + if (base_info == NULL) { + git_error_set(GIT_ERROR_INDEXER, "failed to map delta information"); + return -1; + } + + git_oid__fromraw(&base, base_info, idx->oid_type); + git_mwindow_close(&w); + + if (has_entry(idx, &base)) + return 0; + + if (inject_object(idx, &base) < 0) + return -1; + + stats->local_objects++; + + return 0; +} + +static int resolve_deltas(git_indexer *idx, git_indexer_progress *stats) +{ + unsigned int i; + int error; + struct delta_info *delta; + int progressed = 0, non_null = 0, progress_cb_result; + + while (idx->deltas.length > 0) { + progressed = 0; + non_null = 0; + git_vector_foreach(&idx->deltas, i, delta) { + git_rawobj obj = {0}; + + if (!delta) + continue; + + non_null = 1; + idx->off = delta->delta_off; + if ((error = git_packfile_unpack(&obj, idx->pack, &idx->off)) < 0) { + if (error == GIT_PASSTHROUGH) { + /* We have not seen the base object, we'll try again later. */ + continue; + } + return -1; + } + + if (idx->do_verify && check_object_connectivity(idx, &obj) < 0) + /* TODO: error? continue? */ + continue; + + if (hash_and_save(idx, &obj, delta->delta_off) < 0) + continue; + + git__free(obj.data); + stats->indexed_objects++; + stats->indexed_deltas++; + progressed = 1; + if ((progress_cb_result = do_progress_callback(idx, stats)) < 0) + return progress_cb_result; + + /* remove from the list */ + git_vector_set(NULL, &idx->deltas, i, NULL); + git__free(delta); + } + + /* if none were actually set, we're done */ + if (!non_null) + break; + + if (!progressed && (fix_thin_pack(idx, stats) < 0)) { + return -1; + } + } + + return 0; +} + +static int update_header_and_rehash(git_indexer *idx, git_indexer_progress *stats) +{ + void *ptr; + size_t chunk = 1024*1024; + off64_t hashed = 0; + git_mwindow *w = NULL; + git_mwindow_file *mwf; + unsigned int left; + + mwf = &idx->pack->mwf; + + git_hash_init(&idx->trailer); + + + /* Update the header to include the number of local objects we injected */ + idx->hdr.hdr_entries = htonl(stats->total_objects + stats->local_objects); + if (write_at(idx, &idx->hdr, 0, sizeof(struct git_pack_header)) < 0) + return -1; + + /* + * We now use the same technique as before to determine the + * hash. We keep reading up to the end and let + * hash_partially() keep the existing trailer out of the + * calculation. + */ + if (git_mwindow_free_all(mwf) < 0) + return -1; + + idx->inbuf_len = 0; + while (hashed < mwf->size) { + ptr = git_mwindow_open(mwf, &w, hashed, chunk, &left); + if (ptr == NULL) + return -1; + + hash_partially(idx, ptr, left); + hashed += left; + + git_mwindow_close(&w); + } + + return 0; +} + +int git_indexer_commit(git_indexer *idx, git_indexer_progress *stats) +{ + git_mwindow *w = NULL; + unsigned int i, long_offsets = 0, left; + int error; + struct git_pack_idx_header hdr; + git_str filename = GIT_STR_INIT; + struct entry *entry; + unsigned char checksum[GIT_HASH_MAX_SIZE]; + git_filebuf index_file = {0}; + void *packfile_trailer; + size_t checksum_size; + int filebuf_hash; + bool mismatch; + + if (!idx->parsed_header) { + git_error_set(GIT_ERROR_INDEXER, "incomplete pack header"); + return -1; + } + + checksum_size = git_hash_size(indexer_hash_algorithm(idx)); + filebuf_hash = git_filebuf_hash_flags(indexer_hash_algorithm(idx)); + GIT_ASSERT(checksum_size); + + /* Test for this before resolve_deltas(), as it plays with idx->off */ + if (idx->off + (ssize_t)checksum_size < idx->pack->mwf.size) { + git_error_set(GIT_ERROR_INDEXER, "unexpected data at the end of the pack"); + return -1; + } + if (idx->off + (ssize_t)checksum_size > idx->pack->mwf.size) { + git_error_set(GIT_ERROR_INDEXER, "missing trailer at the end of the pack"); + return -1; + } + + packfile_trailer = git_mwindow_open(&idx->pack->mwf, &w, idx->pack->mwf.size - checksum_size, checksum_size, &left); + if (packfile_trailer == NULL) { + git_mwindow_close(&w); + goto on_error; + } + + /* Compare the packfile trailer as it was sent to us and what we calculated */ + git_hash_final(checksum, &idx->trailer); + mismatch = !!memcmp(checksum, packfile_trailer, checksum_size); + git_mwindow_close(&w); + + if (mismatch) { + git_error_set(GIT_ERROR_INDEXER, "packfile trailer mismatch"); + return -1; + } + + /* Freeze the number of deltas */ + stats->total_deltas = stats->total_objects - stats->indexed_objects; + + if ((error = resolve_deltas(idx, stats)) < 0) + return error; + + if (stats->indexed_objects != stats->total_objects) { + git_error_set(GIT_ERROR_INDEXER, "early EOF"); + return -1; + } + + if (stats->local_objects > 0) { + if (update_header_and_rehash(idx, stats) < 0) + return -1; + + git_hash_final(checksum, &idx->trailer); + write_at(idx, checksum, idx->pack->mwf.size - checksum_size, checksum_size); + } + + /* + * Is the resulting graph fully connected or are we still + * missing some objects? In the second case, we can + * bail out due to an incomplete and thus corrupt + * packfile. + */ + if (git_oidmap_size(idx->expected_oids) > 0) { + git_error_set(GIT_ERROR_INDEXER, "packfile is missing %"PRIuZ" objects", + git_oidmap_size(idx->expected_oids)); + return -1; + } + + git_vector_sort(&idx->objects); + + /* Use the trailer hash as the pack file name to ensure + * files with different contents have different names */ + memcpy(idx->checksum, checksum, checksum_size); + if (git_hash_fmt(idx->name, checksum, checksum_size) < 0) + return -1; + + git_str_sets(&filename, idx->pack->pack_name); + git_str_shorten(&filename, strlen("pack")); + git_str_puts(&filename, "idx"); + if (git_str_oom(&filename)) + return -1; + + if (git_filebuf_open(&index_file, filename.ptr, + filebuf_hash | (idx->do_fsync ? GIT_FILEBUF_FSYNC : 0), + idx->mode) < 0) + goto on_error; + + /* Write out the header */ + hdr.idx_signature = htonl(PACK_IDX_SIGNATURE); + hdr.idx_version = htonl(2); + git_filebuf_write(&index_file, &hdr, sizeof(hdr)); + + /* Write out the fanout table */ + for (i = 0; i < 256; ++i) { + uint32_t n = htonl(idx->fanout[i]); + git_filebuf_write(&index_file, &n, sizeof(n)); + } + + /* Write out the object names (SHA-1 hashes) */ + git_vector_foreach(&idx->objects, i, entry) { + git_filebuf_write(&index_file, &entry->oid.id, git_oid_size(idx->oid_type)); + } + + /* Write out the CRC32 values */ + git_vector_foreach(&idx->objects, i, entry) { + git_filebuf_write(&index_file, &entry->crc, sizeof(uint32_t)); + } + + /* Write out the offsets */ + git_vector_foreach(&idx->objects, i, entry) { + uint32_t n; + + if (entry->offset == UINT32_MAX) + n = htonl(0x80000000 | long_offsets++); + else + n = htonl(entry->offset); + + git_filebuf_write(&index_file, &n, sizeof(uint32_t)); + } + + /* Write out the long offsets */ + git_vector_foreach(&idx->objects, i, entry) { + uint32_t split[2]; + + if (entry->offset != UINT32_MAX) + continue; + + split[0] = htonl(entry->offset_long >> 32); + split[1] = htonl(entry->offset_long & 0xffffffff); + + git_filebuf_write(&index_file, &split, sizeof(uint32_t) * 2); + } + + /* Write out the packfile trailer to the index */ + if (git_filebuf_write(&index_file, checksum, checksum_size) < 0) + goto on_error; + + /* Write out the hash of the idx */ + if (git_filebuf_hash(checksum, &index_file) < 0) + goto on_error; + + git_filebuf_write(&index_file, checksum, checksum_size); + + /* Figure out what the final name should be */ + if (index_path(&filename, idx, ".idx") < 0) + goto on_error; + + /* Commit file */ + if (git_filebuf_commit_at(&index_file, filename.ptr) < 0) + goto on_error; + + if (git_mwindow_free_all(&idx->pack->mwf) < 0) + goto on_error; + +#if !defined(NO_MMAP) && defined(GIT_WIN32) + /* + * Some non-Windows remote filesystems fail when truncating files if the + * file permissions change after opening the file (done by p_mkstemp). + * + * Truncation is only needed when mmap is used to undo rounding up to next + * page_size in append_to_pack. + */ + if (p_ftruncate(idx->pack->mwf.fd, idx->pack->mwf.size) < 0) { + git_error_set(GIT_ERROR_OS, "failed to truncate pack file '%s'", idx->pack->pack_name); + return -1; + } +#endif + + if (idx->do_fsync && p_fsync(idx->pack->mwf.fd) < 0) { + git_error_set(GIT_ERROR_OS, "failed to fsync packfile"); + goto on_error; + } + + /* We need to close the descriptor here so Windows doesn't choke on commit_at */ + if (p_close(idx->pack->mwf.fd) < 0) { + git_error_set(GIT_ERROR_OS, "failed to close packfile"); + goto on_error; + } + + idx->pack->mwf.fd = -1; + + if (index_path(&filename, idx, ".pack") < 0) + goto on_error; + + /* And don't forget to rename the packfile to its new place. */ + if (p_rename(idx->pack->pack_name, git_str_cstr(&filename)) < 0) + goto on_error; + + /* And fsync the parent directory if we're asked to. */ + if (idx->do_fsync && + git_futils_fsync_parent(git_str_cstr(&filename)) < 0) + goto on_error; + + idx->pack_committed = 1; + + git_str_dispose(&filename); + return 0; + +on_error: + git_mwindow_free_all(&idx->pack->mwf); + git_filebuf_cleanup(&index_file); + git_str_dispose(&filename); + return -1; +} + +void git_indexer_free(git_indexer *idx) +{ + const git_oid *key; + git_oid *value; + size_t iter; + + if (idx == NULL) + return; + + if (idx->have_stream) + git_packfile_stream_dispose(&idx->stream); + + git_vector_free_deep(&idx->objects); + + if (idx->pack->idx_cache) { + struct git_pack_entry *pentry; + git_oidmap_foreach_value(idx->pack->idx_cache, pentry, { + git__free(pentry); + }); + + git_oidmap_free(idx->pack->idx_cache); + } + + git_vector_free_deep(&idx->deltas); + + git_packfile_free(idx->pack, !idx->pack_committed); + + iter = 0; + while (git_oidmap_iterate((void **) &value, idx->expected_oids, &iter, &key) == 0) + git__free(value); + + git_hash_ctx_cleanup(&idx->trailer); + git_hash_ctx_cleanup(&idx->hash_ctx); + git_str_dispose(&idx->entry_data); + git_oidmap_free(idx->expected_oids); + git__free(idx); +} diff --git a/src/libgit2/indexer.h b/src/libgit2/indexer.h new file mode 100644 index 0000000..8ee6115 --- /dev/null +++ b/src/libgit2/indexer.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_indexer_h__ +#define INCLUDE_indexer_h__ + +#include "common.h" + +#include "git2/indexer.h" + +extern void git_indexer__set_fsync(git_indexer *idx, int do_fsync); + +#endif diff --git a/src/libgit2/iterator.c b/src/libgit2/iterator.c new file mode 100644 index 0000000..95ded10 --- /dev/null +++ b/src/libgit2/iterator.c @@ -0,0 +1,2456 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "iterator.h" + +#include "tree.h" +#include "index.h" +#include "path.h" + +#define GIT_ITERATOR_FIRST_ACCESS (1 << 15) +#define GIT_ITERATOR_HONOR_IGNORES (1 << 16) +#define GIT_ITERATOR_IGNORE_DOT_GIT (1 << 17) + +#define iterator__flag(I,F) ((((git_iterator *)(I))->flags & GIT_ITERATOR_ ## F) != 0) +#define iterator__ignore_case(I) iterator__flag(I,IGNORE_CASE) +#define iterator__include_trees(I) iterator__flag(I,INCLUDE_TREES) +#define iterator__dont_autoexpand(I) iterator__flag(I,DONT_AUTOEXPAND) +#define iterator__do_autoexpand(I) !iterator__flag(I,DONT_AUTOEXPAND) +#define iterator__include_conflicts(I) iterator__flag(I,INCLUDE_CONFLICTS) +#define iterator__has_been_accessed(I) iterator__flag(I,FIRST_ACCESS) +#define iterator__honor_ignores(I) iterator__flag(I,HONOR_IGNORES) +#define iterator__ignore_dot_git(I) iterator__flag(I,IGNORE_DOT_GIT) +#define iterator__descend_symlinks(I) iterator__flag(I,DESCEND_SYMLINKS) + + +static void iterator_set_ignore_case(git_iterator *iter, bool ignore_case) +{ + if (ignore_case) + iter->flags |= GIT_ITERATOR_IGNORE_CASE; + else + iter->flags &= ~GIT_ITERATOR_IGNORE_CASE; + + iter->strcomp = ignore_case ? git__strcasecmp : git__strcmp; + iter->strncomp = ignore_case ? git__strncasecmp : git__strncmp; + iter->prefixcomp = ignore_case ? git__prefixcmp_icase : git__prefixcmp; + iter->entry_srch = ignore_case ? git_index_entry_isrch : git_index_entry_srch; + + git_vector_set_cmp(&iter->pathlist, (git_vector_cmp)iter->strcomp); +} + +static int iterator_range_init( + git_iterator *iter, const char *start, const char *end) +{ + if (start && *start) { + iter->start = git__strdup(start); + GIT_ERROR_CHECK_ALLOC(iter->start); + + iter->start_len = strlen(iter->start); + } + + if (end && *end) { + iter->end = git__strdup(end); + GIT_ERROR_CHECK_ALLOC(iter->end); + + iter->end_len = strlen(iter->end); + } + + iter->started = (iter->start == NULL); + iter->ended = false; + + return 0; +} + +static void iterator_range_free(git_iterator *iter) +{ + if (iter->start) { + git__free(iter->start); + iter->start = NULL; + iter->start_len = 0; + } + + if (iter->end) { + git__free(iter->end); + iter->end = NULL; + iter->end_len = 0; + } +} + +static int iterator_reset_range( + git_iterator *iter, const char *start, const char *end) +{ + iterator_range_free(iter); + return iterator_range_init(iter, start, end); +} + +static int iterator_pathlist_init(git_iterator *iter, git_strarray *pathlist) +{ + size_t i; + + if (git_vector_init(&iter->pathlist, pathlist->count, NULL) < 0) + return -1; + + for (i = 0; i < pathlist->count; i++) { + if (!pathlist->strings[i]) + continue; + + if (git_vector_insert(&iter->pathlist, pathlist->strings[i]) < 0) + return -1; + } + + return 0; +} + +static int iterator_init_common( + git_iterator *iter, + git_repository *repo, + git_index *index, + git_iterator_options *given_opts) +{ + static git_iterator_options default_opts = GIT_ITERATOR_OPTIONS_INIT; + git_iterator_options *options = given_opts ? given_opts : &default_opts; + bool ignore_case; + int precompose; + int error; + + iter->repo = repo; + iter->index = index; + iter->flags = options->flags; + + if ((iter->flags & GIT_ITERATOR_IGNORE_CASE) != 0) { + ignore_case = true; + } else if ((iter->flags & GIT_ITERATOR_DONT_IGNORE_CASE) != 0) { + ignore_case = false; + } else if (repo) { + git_index *index; + + if ((error = git_repository_index__weakptr(&index, iter->repo)) < 0) + return error; + + ignore_case = !!index->ignore_case; + + if (ignore_case == 1) + iter->flags |= GIT_ITERATOR_IGNORE_CASE; + else + iter->flags |= GIT_ITERATOR_DONT_IGNORE_CASE; + } else { + ignore_case = false; + } + + /* try to look up precompose and set flag if appropriate */ + if (repo && + (iter->flags & GIT_ITERATOR_PRECOMPOSE_UNICODE) == 0 && + (iter->flags & GIT_ITERATOR_DONT_PRECOMPOSE_UNICODE) == 0) { + + if (git_repository__configmap_lookup(&precompose, repo, GIT_CONFIGMAP_PRECOMPOSE) < 0) + git_error_clear(); + else if (precompose) + iter->flags |= GIT_ITERATOR_PRECOMPOSE_UNICODE; + } + + if ((iter->flags & GIT_ITERATOR_DONT_AUTOEXPAND)) + iter->flags |= GIT_ITERATOR_INCLUDE_TREES; + + if ((error = iterator_range_init(iter, options->start, options->end)) < 0 || + (error = iterator_pathlist_init(iter, &options->pathlist)) < 0) + return error; + + iterator_set_ignore_case(iter, ignore_case); + return 0; +} + +static void iterator_clear(git_iterator *iter) +{ + iter->started = false; + iter->ended = false; + iter->stat_calls = 0; + iter->pathlist_walk_idx = 0; + iter->flags &= ~GIT_ITERATOR_FIRST_ACCESS; +} + +GIT_INLINE(bool) iterator_has_started( + git_iterator *iter, const char *path, bool is_submodule) +{ + size_t path_len; + + if (iter->start == NULL || iter->started == true) + return true; + + /* the starting path is generally a prefix - we have started once we + * are prefixed by this path + */ + iter->started = (iter->prefixcomp(path, iter->start) >= 0); + + if (iter->started) + return true; + + path_len = strlen(path); + + /* if, however, we are a submodule, then we support `start` being + * suffixed with a `/` for crazy legacy reasons. match `submod` + * with a start path of `submod/`. + */ + if (is_submodule && iter->start_len && path_len == iter->start_len - 1 && + iter->start[iter->start_len-1] == '/') + return true; + + /* if, however, our current path is a directory, and our starting path + * is _beneath_ that directory, then recurse into the directory (even + * though we have not yet "started") + */ + if (path_len > 0 && path[path_len-1] == '/' && + iter->strncomp(path, iter->start, path_len) == 0) + return true; + + return false; +} + +GIT_INLINE(bool) iterator_has_ended(git_iterator *iter, const char *path) +{ + if (iter->end == NULL) + return false; + else if (iter->ended) + return true; + + iter->ended = (iter->prefixcomp(path, iter->end) > 0); + return iter->ended; +} + +/* walker for the index and tree iterator that allows it to walk the sorted + * pathlist entries alongside sorted iterator entries. + */ +static bool iterator_pathlist_next_is(git_iterator *iter, const char *path) +{ + char *p; + size_t path_len, p_len, cmp_len, i; + int cmp; + + if (iter->pathlist.length == 0) + return true; + + git_vector_sort(&iter->pathlist); + + path_len = strlen(path); + + /* for comparison, drop the trailing slash on the current '/' */ + if (path_len && path[path_len-1] == '/') + path_len--; + + for (i = iter->pathlist_walk_idx; i < iter->pathlist.length; i++) { + p = iter->pathlist.contents[i]; + p_len = strlen(p); + + if (p_len && p[p_len-1] == '/') + p_len--; + + cmp_len = min(path_len, p_len); + + /* see if the pathlist entry is a prefix of this path */ + cmp = iter->strncomp(p, path, cmp_len); + + /* prefix match - see if there's an exact match, or if we were + * given a path that matches the directory + */ + if (cmp == 0) { + /* if this pathlist entry is not suffixed with a '/' then + * it matches a path that is a file or a directory. + * (eg, pathlist = "foo" and path is "foo" or "foo/" or + * "foo/something") + */ + if (p[cmp_len] == '\0' && + (path[cmp_len] == '\0' || path[cmp_len] == '/')) + return true; + + /* if this pathlist entry _is_ suffixed with a '/' then + * it matches only paths that are directories. + * (eg, pathlist = "foo/" and path is "foo/" or "foo/something") + */ + if (p[cmp_len] == '/' && path[cmp_len] == '/') + return true; + } + + /* this pathlist entry sorts before the given path, try the next */ + else if (cmp < 0) { + iter->pathlist_walk_idx++; + continue; + } + + /* this pathlist sorts after the given path, no match. */ + else if (cmp > 0) { + break; + } + } + + return false; +} + +typedef enum { + ITERATOR_PATHLIST_NONE = 0, + ITERATOR_PATHLIST_IS_FILE = 1, + ITERATOR_PATHLIST_IS_DIR = 2, + ITERATOR_PATHLIST_IS_PARENT = 3, + ITERATOR_PATHLIST_FULL = 4 +} iterator_pathlist_search_t; + +static iterator_pathlist_search_t iterator_pathlist_search( + git_iterator *iter, const char *path, size_t path_len) +{ + const char *p; + size_t idx; + int error; + + if (iter->pathlist.length == 0) + return ITERATOR_PATHLIST_FULL; + + git_vector_sort(&iter->pathlist); + + error = git_vector_bsearch2(&idx, &iter->pathlist, + (git_vector_cmp)iter->strcomp, path); + + /* the given path was found in the pathlist. since the pathlist only + * matches directories when they're suffixed with a '/', analyze the + * path string to determine whether it's a directory or not. + */ + if (error == 0) { + if (path_len && path[path_len-1] == '/') + return ITERATOR_PATHLIST_IS_DIR; + + return ITERATOR_PATHLIST_IS_FILE; + } + + /* at this point, the path we're examining may be a directory (though we + * don't know that yet, since we're avoiding a stat unless it's necessary) + * so walk the pathlist looking for the given path with a '/' after it, + */ + while ((p = git_vector_get(&iter->pathlist, idx)) != NULL) { + if (iter->prefixcomp(p, path) != 0) + break; + + /* an exact match would have been matched by the bsearch above */ + GIT_ASSERT_WITH_RETVAL(p[path_len], ITERATOR_PATHLIST_NONE); + + /* is this a literal directory entry (eg `foo/`) or a file beneath */ + if (p[path_len] == '/') { + return (p[path_len+1] == '\0') ? + ITERATOR_PATHLIST_IS_DIR : + ITERATOR_PATHLIST_IS_PARENT; + } + + if (p[path_len] > '/') + break; + + idx++; + } + + return ITERATOR_PATHLIST_NONE; +} + +/* Empty iterator */ + +static int empty_iterator_noop(const git_index_entry **e, git_iterator *i) +{ + GIT_UNUSED(i); + + if (e) + *e = NULL; + + return GIT_ITEROVER; +} + +static int empty_iterator_advance_over( + const git_index_entry **e, + git_iterator_status_t *s, + git_iterator *i) +{ + *s = GIT_ITERATOR_STATUS_EMPTY; + return empty_iterator_noop(e, i); +} + +static int empty_iterator_reset(git_iterator *i) +{ + GIT_UNUSED(i); + return 0; +} + +static void empty_iterator_free(git_iterator *i) +{ + GIT_UNUSED(i); +} + +typedef struct { + git_iterator base; + git_iterator_callbacks cb; +} empty_iterator; + +int git_iterator_for_nothing( + git_iterator **out, + git_iterator_options *options) +{ + empty_iterator *iter; + + static git_iterator_callbacks callbacks = { + empty_iterator_noop, + empty_iterator_noop, + empty_iterator_noop, + empty_iterator_advance_over, + empty_iterator_reset, + empty_iterator_free + }; + + *out = NULL; + + iter = git__calloc(1, sizeof(empty_iterator)); + GIT_ERROR_CHECK_ALLOC(iter); + + iter->base.type = GIT_ITERATOR_EMPTY; + iter->base.cb = &callbacks; + iter->base.flags = options->flags; + + *out = &iter->base; + return 0; +} + +/* Tree iterator */ + +typedef struct { + git_tree_entry *tree_entry; + const char *parent_path; +} tree_iterator_entry; + +typedef struct { + git_tree *tree; + + /* path to this particular frame (folder) */ + git_str path; + + /* a sorted list of the entries for this frame (folder), these are + * actually pointers to the iterator's entry pool. + */ + git_vector entries; + tree_iterator_entry *current; + + size_t next_idx; + + /* on case insensitive iterations, we also have an array of other + * paths that were case insensitively equal to this one, and their + * tree objects. we have coalesced the tree entries into this frame. + * a child `tree_iterator_entry` will contain a pointer to its actual + * parent path. + */ + git_vector similar_trees; + git_array_t(git_str) similar_paths; +} tree_iterator_frame; + +typedef struct { + git_iterator base; + git_tree *root; + git_array_t(tree_iterator_frame) frames; + + git_index_entry entry; + git_str entry_path; + + /* a pool of entries to reduce the number of allocations */ + git_pool entry_pool; +} tree_iterator; + +GIT_INLINE(tree_iterator_frame *) tree_iterator_parent_frame( + tree_iterator *iter) +{ + return iter->frames.size > 1 ? + &iter->frames.ptr[iter->frames.size-2] : NULL; +} + +GIT_INLINE(tree_iterator_frame *) tree_iterator_current_frame( + tree_iterator *iter) +{ + return iter->frames.size ? &iter->frames.ptr[iter->frames.size-1] : NULL; +} + +GIT_INLINE(int) tree_entry_cmp( + const git_tree_entry *a, const git_tree_entry *b, bool icase) +{ + return git_fs_path_cmp( + a->filename, a->filename_len, a->attr == GIT_FILEMODE_TREE, + b->filename, b->filename_len, b->attr == GIT_FILEMODE_TREE, + icase ? git__strncasecmp : git__strncmp); +} + +GIT_INLINE(int) tree_iterator_entry_cmp_icase( + const void *ptr_a, const void *ptr_b) +{ + const tree_iterator_entry *a = (const tree_iterator_entry *)ptr_a; + const tree_iterator_entry *b = (const tree_iterator_entry *)ptr_b; + + return tree_entry_cmp(a->tree_entry, b->tree_entry, true); +} + +static int tree_iterator_entry_sort_icase(const void *ptr_a, const void *ptr_b) +{ + const tree_iterator_entry *a = (const tree_iterator_entry *)ptr_a; + const tree_iterator_entry *b = (const tree_iterator_entry *)ptr_b; + + int c = tree_entry_cmp(a->tree_entry, b->tree_entry, true); + + /* stabilize the sort order for filenames that are (case insensitively) + * the same by examining the parent path (case sensitively) before + * falling back to a case sensitive sort of the filename. + */ + if (!c && a->parent_path != b->parent_path) + c = git__strcmp(a->parent_path, b->parent_path); + + if (!c) + c = tree_entry_cmp(a->tree_entry, b->tree_entry, false); + + return c; +} + +static int tree_iterator_compute_path( + git_str *out, + tree_iterator_entry *entry) +{ + git_str_clear(out); + + if (entry->parent_path) + git_str_joinpath(out, entry->parent_path, entry->tree_entry->filename); + else + git_str_puts(out, entry->tree_entry->filename); + + if (git_tree_entry__is_tree(entry->tree_entry)) + git_str_putc(out, '/'); + + if (git_str_oom(out)) + return -1; + + return 0; +} + +static int tree_iterator_frame_init( + tree_iterator *iter, + git_tree *tree, + tree_iterator_entry *frame_entry) +{ + tree_iterator_frame *new_frame = NULL; + tree_iterator_entry *new_entry; + git_tree *dup = NULL; + git_tree_entry *tree_entry; + git_vector_cmp cmp; + size_t i; + int error = 0; + + new_frame = git_array_alloc(iter->frames); + GIT_ERROR_CHECK_ALLOC(new_frame); + + if ((error = git_tree_dup(&dup, tree)) < 0) + goto done; + + memset(new_frame, 0x0, sizeof(tree_iterator_frame)); + new_frame->tree = dup; + + if (frame_entry && + (error = tree_iterator_compute_path(&new_frame->path, frame_entry)) < 0) + goto done; + + cmp = iterator__ignore_case(&iter->base) ? + tree_iterator_entry_sort_icase : NULL; + + if ((error = git_vector_init(&new_frame->entries, + dup->entries.size, cmp)) < 0) + goto done; + + git_array_foreach(dup->entries, i, tree_entry) { + if ((new_entry = git_pool_malloc(&iter->entry_pool, 1)) == NULL) { + git_error_set_oom(); + error = -1; + goto done; + } + + new_entry->tree_entry = tree_entry; + new_entry->parent_path = new_frame->path.ptr; + + if ((error = git_vector_insert(&new_frame->entries, new_entry)) < 0) + goto done; + } + + git_vector_set_sorted(&new_frame->entries, + !iterator__ignore_case(&iter->base)); + +done: + if (error < 0) { + git_tree_free(dup); + git_array_pop(iter->frames); + } + + return error; +} + +GIT_INLINE(tree_iterator_entry *) tree_iterator_current_entry( + tree_iterator_frame *frame) +{ + return frame->current; +} + +GIT_INLINE(int) tree_iterator_frame_push_neighbors( + tree_iterator *iter, + tree_iterator_frame *parent_frame, + tree_iterator_frame *frame, + const char *filename) +{ + tree_iterator_entry *entry, *new_entry; + git_tree *tree = NULL; + git_tree_entry *tree_entry; + git_str *path; + size_t new_size, i; + int error = 0; + + while (parent_frame->next_idx < parent_frame->entries.length) { + entry = parent_frame->entries.contents[parent_frame->next_idx]; + + if (strcasecmp(filename, entry->tree_entry->filename) != 0) + break; + + if ((error = git_tree_lookup(&tree, + iter->base.repo, &entry->tree_entry->oid)) < 0) + break; + + if (git_vector_insert(&parent_frame->similar_trees, tree) < 0) + break; + + path = git_array_alloc(parent_frame->similar_paths); + GIT_ERROR_CHECK_ALLOC(path); + + memset(path, 0, sizeof(git_str)); + + if ((error = tree_iterator_compute_path(path, entry)) < 0) + break; + + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, + frame->entries.length, tree->entries.size); + git_vector_size_hint(&frame->entries, new_size); + + git_array_foreach(tree->entries, i, tree_entry) { + new_entry = git_pool_malloc(&iter->entry_pool, 1); + GIT_ERROR_CHECK_ALLOC(new_entry); + + new_entry->tree_entry = tree_entry; + new_entry->parent_path = path->ptr; + + if ((error = git_vector_insert(&frame->entries, new_entry)) < 0) + break; + } + + if (error) + break; + + parent_frame->next_idx++; + } + + return error; +} + +GIT_INLINE(int) tree_iterator_frame_push( + tree_iterator *iter, tree_iterator_entry *entry) +{ + tree_iterator_frame *parent_frame, *frame; + git_tree *tree = NULL; + int error; + + if ((error = git_tree_lookup(&tree, + iter->base.repo, &entry->tree_entry->oid)) < 0 || + (error = tree_iterator_frame_init(iter, tree, entry)) < 0) + goto done; + + parent_frame = tree_iterator_parent_frame(iter); + frame = tree_iterator_current_frame(iter); + + /* if we're case insensitive, then we may have another directory that + * is (case insensitively) equal to this one. coalesce those children + * into this tree. + */ + if (iterator__ignore_case(&iter->base)) + error = tree_iterator_frame_push_neighbors(iter, + parent_frame, frame, entry->tree_entry->filename); + +done: + git_tree_free(tree); + return error; +} + +static int tree_iterator_frame_pop(tree_iterator *iter) +{ + tree_iterator_frame *frame; + git_str *buf = NULL; + git_tree *tree; + size_t i; + + GIT_ASSERT(iter->frames.size); + + frame = git_array_pop(iter->frames); + + git_vector_free(&frame->entries); + git_tree_free(frame->tree); + + do { + buf = git_array_pop(frame->similar_paths); + git_str_dispose(buf); + } while (buf != NULL); + + git_array_clear(frame->similar_paths); + + git_vector_foreach(&frame->similar_trees, i, tree) + git_tree_free(tree); + + git_vector_free(&frame->similar_trees); + + git_str_dispose(&frame->path); + + return 0; +} + +static int tree_iterator_current( + const git_index_entry **out, git_iterator *i) +{ + tree_iterator *iter = (tree_iterator *)i; + + if (!iterator__has_been_accessed(i)) + return iter->base.cb->advance(out, i); + + if (!iter->frames.size) { + *out = NULL; + return GIT_ITEROVER; + } + + *out = &iter->entry; + return 0; +} + +static void tree_iterator_set_current( + tree_iterator *iter, + tree_iterator_frame *frame, + tree_iterator_entry *entry) +{ + git_tree_entry *tree_entry = entry->tree_entry; + + frame->current = entry; + + memset(&iter->entry, 0x0, sizeof(git_index_entry)); + + iter->entry.mode = tree_entry->attr; + iter->entry.path = iter->entry_path.ptr; + git_oid_cpy(&iter->entry.id, &tree_entry->oid); +} + +static int tree_iterator_advance(const git_index_entry **out, git_iterator *i) +{ + tree_iterator *iter = (tree_iterator *)i; + int error = 0; + + iter->base.flags |= GIT_ITERATOR_FIRST_ACCESS; + + /* examine tree entries until we find the next one to return */ + while (true) { + tree_iterator_entry *prev_entry, *entry; + tree_iterator_frame *frame; + bool is_tree; + + if ((frame = tree_iterator_current_frame(iter)) == NULL) { + error = GIT_ITEROVER; + break; + } + + /* no more entries in this frame. pop the frame out */ + if (frame->next_idx == frame->entries.length) { + if ((error = tree_iterator_frame_pop(iter)) < 0) + break; + + continue; + } + + /* we may have coalesced the contents of case-insensitively same-named + * directories, so do the sort now. + */ + if (frame->next_idx == 0 && !git_vector_is_sorted(&frame->entries)) + git_vector_sort(&frame->entries); + + /* we have more entries in the current frame, that's our next entry */ + prev_entry = tree_iterator_current_entry(frame); + entry = frame->entries.contents[frame->next_idx]; + frame->next_idx++; + + /* we can have collisions when iterating case insensitively. (eg, + * 'A/a' and 'a/A'). squash this one if it's already been seen. + */ + if (iterator__ignore_case(&iter->base) && + prev_entry && + tree_iterator_entry_cmp_icase(prev_entry, entry) == 0) + continue; + + if ((error = tree_iterator_compute_path(&iter->entry_path, entry)) < 0) + break; + + /* if this path is before our start, advance over this entry */ + if (!iterator_has_started(&iter->base, iter->entry_path.ptr, false)) + continue; + + /* if this path is after our end, stop */ + if (iterator_has_ended(&iter->base, iter->entry_path.ptr)) { + error = GIT_ITEROVER; + break; + } + + /* if we have a list of paths we're interested in, examine it */ + if (!iterator_pathlist_next_is(&iter->base, iter->entry_path.ptr)) + continue; + + is_tree = git_tree_entry__is_tree(entry->tree_entry); + + /* if we are *not* including trees then advance over this entry */ + if (is_tree && !iterator__include_trees(iter)) { + + /* if we've found a tree (and are not returning it to the caller) + * and we are autoexpanding, then we want to return the first + * child. push the new directory and advance. + */ + if (iterator__do_autoexpand(iter)) { + if ((error = tree_iterator_frame_push(iter, entry)) < 0) + break; + } + + continue; + } + + tree_iterator_set_current(iter, frame, entry); + + /* if we are autoexpanding, then push this as a new frame, so that + * the next call to `advance` will dive into this directory. + */ + if (is_tree && iterator__do_autoexpand(iter)) + error = tree_iterator_frame_push(iter, entry); + + break; + } + + if (out) + *out = (error == 0) ? &iter->entry : NULL; + + return error; +} + +static int tree_iterator_advance_into( + const git_index_entry **out, git_iterator *i) +{ + tree_iterator *iter = (tree_iterator *)i; + tree_iterator_frame *frame; + tree_iterator_entry *prev_entry; + int error; + + if (out) + *out = NULL; + + if ((frame = tree_iterator_current_frame(iter)) == NULL) + return GIT_ITEROVER; + + /* get the last seen entry */ + prev_entry = tree_iterator_current_entry(frame); + + /* it's legal to call advance_into when auto-expand is on. in this case, + * we will have pushed a new (empty) frame on to the stack for this + * new directory. since it's empty, its current_entry should be null. + */ + GIT_ASSERT(iterator__do_autoexpand(i) ^ (prev_entry != NULL)); + + if (prev_entry) { + if (!git_tree_entry__is_tree(prev_entry->tree_entry)) + return 0; + + if ((error = tree_iterator_frame_push(iter, prev_entry)) < 0) + return error; + } + + /* we've advanced into the directory in question, let advance + * find the first entry + */ + return tree_iterator_advance(out, i); +} + +static int tree_iterator_advance_over( + const git_index_entry **out, + git_iterator_status_t *status, + git_iterator *i) +{ + *status = GIT_ITERATOR_STATUS_NORMAL; + return git_iterator_advance(out, i); +} + +static void tree_iterator_clear(tree_iterator *iter) +{ + while (iter->frames.size) + tree_iterator_frame_pop(iter); + + git_array_clear(iter->frames); + + git_pool_clear(&iter->entry_pool); + git_str_clear(&iter->entry_path); + + iterator_clear(&iter->base); +} + +static int tree_iterator_init(tree_iterator *iter) +{ + int error; + + if ((error = git_pool_init(&iter->entry_pool, sizeof(tree_iterator_entry))) < 0 || + (error = tree_iterator_frame_init(iter, iter->root, NULL)) < 0) + return error; + + iter->base.flags &= ~GIT_ITERATOR_FIRST_ACCESS; + + return 0; +} + +static int tree_iterator_reset(git_iterator *i) +{ + tree_iterator *iter = (tree_iterator *)i; + + tree_iterator_clear(iter); + return tree_iterator_init(iter); +} + +static void tree_iterator_free(git_iterator *i) +{ + tree_iterator *iter = (tree_iterator *)i; + + tree_iterator_clear(iter); + + git_tree_free(iter->root); + git_str_dispose(&iter->entry_path); +} + +int git_iterator_for_tree( + git_iterator **out, + git_tree *tree, + git_iterator_options *options) +{ + tree_iterator *iter; + int error; + + static git_iterator_callbacks callbacks = { + tree_iterator_current, + tree_iterator_advance, + tree_iterator_advance_into, + tree_iterator_advance_over, + tree_iterator_reset, + tree_iterator_free + }; + + *out = NULL; + + if (tree == NULL) + return git_iterator_for_nothing(out, options); + + iter = git__calloc(1, sizeof(tree_iterator)); + GIT_ERROR_CHECK_ALLOC(iter); + + iter->base.type = GIT_ITERATOR_TREE; + iter->base.cb = &callbacks; + + if ((error = iterator_init_common(&iter->base, + git_tree_owner(tree), NULL, options)) < 0 || + (error = git_tree_dup(&iter->root, tree)) < 0 || + (error = tree_iterator_init(iter)) < 0) + goto on_error; + + *out = &iter->base; + return 0; + +on_error: + git_iterator_free(&iter->base); + return error; +} + +int git_iterator_current_tree_entry( + const git_tree_entry **tree_entry, git_iterator *i) +{ + tree_iterator *iter; + tree_iterator_frame *frame; + tree_iterator_entry *entry; + + GIT_ASSERT(i->type == GIT_ITERATOR_TREE); + + iter = (tree_iterator *)i; + + frame = tree_iterator_current_frame(iter); + entry = tree_iterator_current_entry(frame); + + *tree_entry = entry->tree_entry; + return 0; +} + +int git_iterator_current_parent_tree( + const git_tree **parent_tree, git_iterator *i, size_t depth) +{ + tree_iterator *iter; + tree_iterator_frame *frame; + + GIT_ASSERT(i->type == GIT_ITERATOR_TREE); + + iter = (tree_iterator *)i; + + GIT_ASSERT(depth < iter->frames.size); + frame = &iter->frames.ptr[iter->frames.size-depth-1]; + + *parent_tree = frame->tree; + return 0; +} + +/* Filesystem iterator */ + +typedef struct { + struct stat st; + size_t path_len; + iterator_pathlist_search_t match; + git_oid id; + char path[GIT_FLEX_ARRAY]; +} filesystem_iterator_entry; + +typedef struct { + git_vector entries; + git_pool entry_pool; + size_t next_idx; + + size_t path_len; + int is_ignored; +} filesystem_iterator_frame; + +typedef struct { + git_iterator base; + char *root; + size_t root_len; + + unsigned int dirload_flags; + + git_tree *tree; + git_index *index; + git_vector index_snapshot; + + git_oid_t oid_type; + + git_array_t(filesystem_iterator_frame) frames; + git_ignores ignores; + + /* info about the current entry */ + git_index_entry entry; + git_str current_path; + int current_is_ignored; + + /* temporary buffer for advance_over */ + git_str tmp_buf; +} filesystem_iterator; + + +GIT_INLINE(filesystem_iterator_frame *) filesystem_iterator_parent_frame( + filesystem_iterator *iter) +{ + return iter->frames.size > 1 ? + &iter->frames.ptr[iter->frames.size-2] : NULL; +} + +GIT_INLINE(filesystem_iterator_frame *) filesystem_iterator_current_frame( + filesystem_iterator *iter) +{ + return iter->frames.size ? &iter->frames.ptr[iter->frames.size-1] : NULL; +} + +GIT_INLINE(filesystem_iterator_entry *) filesystem_iterator_current_entry( + filesystem_iterator_frame *frame) +{ + return frame->next_idx == 0 ? + NULL : frame->entries.contents[frame->next_idx-1]; +} + +static int filesystem_iterator_entry_cmp(const void *_a, const void *_b) +{ + const filesystem_iterator_entry *a = (const filesystem_iterator_entry *)_a; + const filesystem_iterator_entry *b = (const filesystem_iterator_entry *)_b; + + return git__strcmp(a->path, b->path); +} + +static int filesystem_iterator_entry_cmp_icase(const void *_a, const void *_b) +{ + const filesystem_iterator_entry *a = (const filesystem_iterator_entry *)_a; + const filesystem_iterator_entry *b = (const filesystem_iterator_entry *)_b; + + return git__strcasecmp(a->path, b->path); +} + +#define FILESYSTEM_MAX_DEPTH 100 + +/** + * Figure out if an entry is a submodule. + * + * We consider it a submodule if the path is listed as a submodule in + * either the tree or the index. + */ +static int filesystem_iterator_is_submodule( + bool *out, filesystem_iterator *iter, const char *path, size_t path_len) +{ + bool is_submodule = false; + int error; + + *out = false; + + /* first see if this path is a submodule in HEAD */ + if (iter->tree) { + git_tree_entry *entry; + + error = git_tree_entry_bypath(&entry, iter->tree, path); + + if (error < 0 && error != GIT_ENOTFOUND) + return error; + + if (!error) { + is_submodule = (entry->attr == GIT_FILEMODE_COMMIT); + git_tree_entry_free(entry); + } + } + + if (!is_submodule && iter->base.index) { + size_t pos; + + error = git_index_snapshot_find(&pos, + &iter->index_snapshot, iter->base.entry_srch, path, path_len, 0); + + if (error < 0 && error != GIT_ENOTFOUND) + return error; + + if (!error) { + git_index_entry *e = git_vector_get(&iter->index_snapshot, pos); + is_submodule = (e->mode == GIT_FILEMODE_COMMIT); + } + } + + *out = is_submodule; + return 0; +} + +static void filesystem_iterator_frame_push_ignores( + filesystem_iterator *iter, + filesystem_iterator_entry *frame_entry, + filesystem_iterator_frame *new_frame) +{ + filesystem_iterator_frame *previous_frame; + const char *path = frame_entry ? frame_entry->path : ""; + + if (!iterator__honor_ignores(&iter->base)) + return; + + if (git_ignore__lookup(&new_frame->is_ignored, + &iter->ignores, path, GIT_DIR_FLAG_TRUE) < 0) { + git_error_clear(); + new_frame->is_ignored = GIT_IGNORE_NOTFOUND; + } + + /* if this is not the top level directory... */ + if (frame_entry) { + const char *relative_path; + + previous_frame = filesystem_iterator_parent_frame(iter); + + /* push new ignores for files in this directory */ + relative_path = frame_entry->path + previous_frame->path_len; + + /* inherit ignored from parent if no rule specified */ + if (new_frame->is_ignored <= GIT_IGNORE_NOTFOUND) + new_frame->is_ignored = previous_frame->is_ignored; + + git_ignore__push_dir(&iter->ignores, relative_path); + } +} + +static void filesystem_iterator_frame_pop_ignores( + filesystem_iterator *iter) +{ + if (iterator__honor_ignores(&iter->base)) + git_ignore__pop_dir(&iter->ignores); +} + +GIT_INLINE(bool) filesystem_iterator_examine_path( + bool *is_dir_out, + iterator_pathlist_search_t *match_out, + filesystem_iterator *iter, + filesystem_iterator_entry *frame_entry, + const char *path, + size_t path_len) +{ + bool is_dir = 0; + iterator_pathlist_search_t match = ITERATOR_PATHLIST_FULL; + + *is_dir_out = false; + *match_out = ITERATOR_PATHLIST_NONE; + + if (iter->base.start_len) { + int cmp = iter->base.strncomp(path, iter->base.start, path_len); + + /* we haven't stat'ed `path` yet, so we don't yet know if it's a + * directory or not. special case if the current path may be a + * directory that matches the start prefix. + */ + if (cmp == 0) { + if (iter->base.start[path_len] == '/') + is_dir = true; + + else if (iter->base.start[path_len] != '\0') + cmp = -1; + } + + if (cmp < 0) + return false; + } + + if (iter->base.end_len) { + int cmp = iter->base.strncomp(path, iter->base.end, iter->base.end_len); + + if (cmp > 0) + return false; + } + + /* if we have a pathlist that we're limiting to, examine this path now + * to avoid a `stat` if we're not interested in the path. + */ + if (iter->base.pathlist.length) { + /* if our parent was explicitly included, so too are we */ + if (frame_entry && frame_entry->match != ITERATOR_PATHLIST_IS_PARENT) + match = ITERATOR_PATHLIST_FULL; + else + match = iterator_pathlist_search(&iter->base, path, path_len); + + if (match == ITERATOR_PATHLIST_NONE) + return false; + + /* Ensure that the pathlist entry lines up with what we expected */ + if (match == ITERATOR_PATHLIST_IS_DIR || + match == ITERATOR_PATHLIST_IS_PARENT) + is_dir = true; + } + + *is_dir_out = is_dir; + *match_out = match; + return true; +} + +GIT_INLINE(bool) filesystem_iterator_is_dot_git( + filesystem_iterator *iter, const char *path, size_t path_len) +{ + size_t len; + + if (!iterator__ignore_dot_git(&iter->base)) + return false; + + if ((len = path_len) < 4) + return false; + + if (path[len - 1] == '/') + len--; + + if (git__tolower(path[len - 1]) != 't' || + git__tolower(path[len - 2]) != 'i' || + git__tolower(path[len - 3]) != 'g' || + git__tolower(path[len - 4]) != '.') + return false; + + return (len == 4 || path[len - 5] == '/'); +} + +static int filesystem_iterator_entry_hash( + filesystem_iterator *iter, + filesystem_iterator_entry *entry) +{ + git_str fullpath = GIT_STR_INIT; + int error; + + if (S_ISDIR(entry->st.st_mode)) { + memset(&entry->id, 0, git_oid_size(iter->oid_type)); + return 0; + } + + if (iter->base.type == GIT_ITERATOR_WORKDIR) + return git_repository_hashfile(&entry->id, + iter->base.repo, entry->path, GIT_OBJECT_BLOB, NULL); + + if (!(error = git_str_joinpath(&fullpath, iter->root, entry->path)) && + !(error = git_path_validate_str_length(iter->base.repo, &fullpath))) + error = git_odb__hashfile(&entry->id, fullpath.ptr, GIT_OBJECT_BLOB, iter->oid_type); + + git_str_dispose(&fullpath); + return error; +} + +static int filesystem_iterator_entry_init( + filesystem_iterator_entry **out, + filesystem_iterator *iter, + filesystem_iterator_frame *frame, + const char *path, + size_t path_len, + struct stat *statbuf, + iterator_pathlist_search_t pathlist_match) +{ + filesystem_iterator_entry *entry; + size_t entry_size; + int error = 0; + + *out = NULL; + + /* Make sure to append two bytes, one for the path's null + * termination, one for a possible trailing '/' for folders. + */ + GIT_ERROR_CHECK_ALLOC_ADD(&entry_size, + sizeof(filesystem_iterator_entry), path_len); + GIT_ERROR_CHECK_ALLOC_ADD(&entry_size, entry_size, 2); + + entry = git_pool_malloc(&frame->entry_pool, entry_size); + GIT_ERROR_CHECK_ALLOC(entry); + + entry->path_len = path_len; + entry->match = pathlist_match; + memcpy(entry->path, path, path_len); + memcpy(&entry->st, statbuf, sizeof(struct stat)); + + /* Suffix directory paths with a '/' */ + if (S_ISDIR(entry->st.st_mode)) + entry->path[entry->path_len++] = '/'; + + entry->path[entry->path_len] = '\0'; + + if (iter->base.flags & GIT_ITERATOR_INCLUDE_HASH) + error = filesystem_iterator_entry_hash(iter, entry); + + if (!error) + *out = entry; + + return error; +} + +static int filesystem_iterator_frame_push( + filesystem_iterator *iter, + filesystem_iterator_entry *frame_entry) +{ + filesystem_iterator_frame *new_frame = NULL; + git_fs_path_diriter diriter = GIT_FS_PATH_DIRITER_INIT; + git_str root = GIT_STR_INIT; + const char *path; + filesystem_iterator_entry *entry; + struct stat statbuf; + size_t path_len; + int error; + + if (iter->frames.size == FILESYSTEM_MAX_DEPTH) { + git_error_set(GIT_ERROR_REPOSITORY, + "directory nesting too deep (%"PRIuZ")", iter->frames.size); + return -1; + } + + new_frame = git_array_alloc(iter->frames); + GIT_ERROR_CHECK_ALLOC(new_frame); + + memset(new_frame, 0, sizeof(filesystem_iterator_frame)); + + if (frame_entry) + git_str_joinpath(&root, iter->root, frame_entry->path); + else + git_str_puts(&root, iter->root); + + if (git_str_oom(&root) || + git_path_validate_str_length(iter->base.repo, &root) < 0) { + error = -1; + goto done; + } + + new_frame->path_len = frame_entry ? frame_entry->path_len : 0; + + /* Any error here is equivalent to the dir not existing, skip over it */ + if ((error = git_fs_path_diriter_init( + &diriter, root.ptr, iter->dirload_flags)) < 0) { + error = GIT_ENOTFOUND; + goto done; + } + + if ((error = git_vector_init(&new_frame->entries, 64, + iterator__ignore_case(&iter->base) ? + filesystem_iterator_entry_cmp_icase : + filesystem_iterator_entry_cmp)) < 0) + goto done; + + if ((error = git_pool_init(&new_frame->entry_pool, 1)) < 0) + goto done; + + /* check if this directory is ignored */ + filesystem_iterator_frame_push_ignores(iter, frame_entry, new_frame); + + while ((error = git_fs_path_diriter_next(&diriter)) == 0) { + iterator_pathlist_search_t pathlist_match = ITERATOR_PATHLIST_FULL; + git_str path_str = GIT_STR_INIT; + bool dir_expected = false; + + if ((error = git_fs_path_diriter_fullpath(&path, &path_len, &diriter)) < 0) + goto done; + + path_str.ptr = (char *)path; + path_str.size = path_len; + + if ((error = git_path_validate_str_length(iter->base.repo, &path_str)) < 0) + goto done; + + GIT_ASSERT(path_len > iter->root_len); + + /* remove the prefix if requested */ + path += iter->root_len; + path_len -= iter->root_len; + + /* examine start / end and the pathlist to see if this path is in it. + * note that since we haven't yet stat'ed the path, we cannot know + * whether it's a directory yet or not, so this can give us an + * expected type (S_IFDIR or S_IFREG) that we should examine) + */ + if (!filesystem_iterator_examine_path(&dir_expected, &pathlist_match, + iter, frame_entry, path, path_len)) + continue; + + /* TODO: don't need to stat if assume unchanged for this path and + * we have an index, we can just copy the data out of it. + */ + + if ((error = git_fs_path_diriter_stat(&statbuf, &diriter)) < 0) { + /* file was removed between readdir and lstat */ + if (error == GIT_ENOTFOUND) + continue; + + /* treat the file as unreadable */ + memset(&statbuf, 0, sizeof(statbuf)); + statbuf.st_mode = GIT_FILEMODE_UNREADABLE; + + error = 0; + } + + iter->base.stat_calls++; + + /* Ignore wacky things in the filesystem */ + if (!S_ISDIR(statbuf.st_mode) && + !S_ISREG(statbuf.st_mode) && + !S_ISLNK(statbuf.st_mode) && + statbuf.st_mode != GIT_FILEMODE_UNREADABLE) + continue; + + if (filesystem_iterator_is_dot_git(iter, path, path_len)) + continue; + + /* convert submodules to GITLINK and remove trailing slashes */ + if (S_ISDIR(statbuf.st_mode)) { + bool submodule = false; + + if ((error = filesystem_iterator_is_submodule(&submodule, + iter, path, path_len)) < 0) + goto done; + + if (submodule) + statbuf.st_mode = GIT_FILEMODE_COMMIT; + } + + /* Ensure that the pathlist entry lines up with what we expected */ + else if (dir_expected) + continue; + + if ((error = filesystem_iterator_entry_init(&entry, + iter, new_frame, path, path_len, &statbuf, pathlist_match)) < 0) + goto done; + + git_vector_insert(&new_frame->entries, entry); + } + + if (error == GIT_ITEROVER) + error = 0; + + /* sort now that directory suffix is added */ + git_vector_sort(&new_frame->entries); + +done: + if (error < 0) + git_array_pop(iter->frames); + + git_str_dispose(&root); + git_fs_path_diriter_free(&diriter); + return error; +} + +GIT_INLINE(int) filesystem_iterator_frame_pop(filesystem_iterator *iter) +{ + filesystem_iterator_frame *frame; + + GIT_ASSERT(iter->frames.size); + + frame = git_array_pop(iter->frames); + filesystem_iterator_frame_pop_ignores(iter); + + git_pool_clear(&frame->entry_pool); + git_vector_free(&frame->entries); + + return 0; +} + +static void filesystem_iterator_set_current( + filesystem_iterator *iter, + filesystem_iterator_entry *entry) +{ + /* + * Index entries are limited to 32 bit timestamps. We can safely + * cast this since workdir times are only used in the cache; any + * mismatch will cause a hash recomputation which is unfortunate + * but affects only people who set their filetimes to 2038. + * (Same with the file size.) + */ + iter->entry.ctime.seconds = (int32_t)entry->st.st_ctime; + iter->entry.mtime.seconds = (int32_t)entry->st.st_mtime; + +#if defined(GIT_USE_NSEC) + iter->entry.ctime.nanoseconds = entry->st.st_ctime_nsec; + iter->entry.mtime.nanoseconds = entry->st.st_mtime_nsec; +#else + iter->entry.ctime.nanoseconds = 0; + iter->entry.mtime.nanoseconds = 0; +#endif + + iter->entry.dev = entry->st.st_dev; + iter->entry.ino = entry->st.st_ino; + iter->entry.mode = git_futils_canonical_mode(entry->st.st_mode); + iter->entry.uid = entry->st.st_uid; + iter->entry.gid = entry->st.st_gid; + iter->entry.file_size = (uint32_t)entry->st.st_size; + + if (iter->base.flags & GIT_ITERATOR_INCLUDE_HASH) + git_oid_cpy(&iter->entry.id, &entry->id); + else + git_oid_clear(&iter->entry.id, iter->oid_type); + + iter->entry.path = entry->path; + + iter->current_is_ignored = GIT_IGNORE_UNCHECKED; +} + +static int filesystem_iterator_current( + const git_index_entry **out, git_iterator *i) +{ + filesystem_iterator *iter = GIT_CONTAINER_OF(i, filesystem_iterator, base); + + if (!iterator__has_been_accessed(i)) + return iter->base.cb->advance(out, i); + + if (!iter->frames.size) { + *out = NULL; + return GIT_ITEROVER; + } + + *out = &iter->entry; + return 0; +} + +static int filesystem_iterator_is_dir( + bool *is_dir, + const filesystem_iterator *iter, + const filesystem_iterator_entry *entry) +{ + struct stat st; + git_str fullpath = GIT_STR_INIT; + int error = 0; + + if (S_ISDIR(entry->st.st_mode)) { + *is_dir = 1; + goto done; + } + + if (!iterator__descend_symlinks(iter) || !S_ISLNK(entry->st.st_mode)) { + *is_dir = 0; + goto done; + } + + if ((error = git_str_joinpath(&fullpath, iter->root, entry->path)) < 0 || + (error = git_path_validate_str_length(iter->base.repo, &fullpath)) < 0 || + (error = p_stat(fullpath.ptr, &st)) < 0) + goto done; + + *is_dir = S_ISDIR(st.st_mode); + +done: + git_str_dispose(&fullpath); + return error; +} + +static int filesystem_iterator_advance( + const git_index_entry **out, git_iterator *i) +{ + filesystem_iterator *iter = GIT_CONTAINER_OF(i, filesystem_iterator, base); + bool is_dir; + int error = 0; + + iter->base.flags |= GIT_ITERATOR_FIRST_ACCESS; + + /* examine filesystem entries until we find the next one to return */ + while (true) { + filesystem_iterator_frame *frame; + filesystem_iterator_entry *entry; + + if ((frame = filesystem_iterator_current_frame(iter)) == NULL) { + error = GIT_ITEROVER; + break; + } + + /* no more entries in this frame. pop the frame out */ + if (frame->next_idx == frame->entries.length) { + filesystem_iterator_frame_pop(iter); + continue; + } + + /* we have more entries in the current frame, that's our next entry */ + entry = frame->entries.contents[frame->next_idx]; + frame->next_idx++; + + if ((error = filesystem_iterator_is_dir(&is_dir, iter, entry)) < 0) + break; + + if (is_dir) { + if (iterator__do_autoexpand(iter)) { + error = filesystem_iterator_frame_push(iter, entry); + + /* may get GIT_ENOTFOUND due to races or permission problems + * that we want to quietly swallow + */ + if (error == GIT_ENOTFOUND) + continue; + else if (error < 0) + break; + } + + if (!iterator__include_trees(iter)) + continue; + } + + filesystem_iterator_set_current(iter, entry); + break; + } + + if (out) + *out = (error == 0) ? &iter->entry : NULL; + + return error; +} + +static int filesystem_iterator_advance_into( + const git_index_entry **out, git_iterator *i) +{ + filesystem_iterator *iter = GIT_CONTAINER_OF(i, filesystem_iterator, base); + filesystem_iterator_frame *frame; + filesystem_iterator_entry *prev_entry; + int error; + + if (out) + *out = NULL; + + if ((frame = filesystem_iterator_current_frame(iter)) == NULL) + return GIT_ITEROVER; + + /* get the last seen entry */ + prev_entry = filesystem_iterator_current_entry(frame); + + /* it's legal to call advance_into when auto-expand is on. in this case, + * we will have pushed a new (empty) frame on to the stack for this + * new directory. since it's empty, its current_entry should be null. + */ + GIT_ASSERT(iterator__do_autoexpand(i) ^ (prev_entry != NULL)); + + if (prev_entry) { + if (prev_entry->st.st_mode != GIT_FILEMODE_COMMIT && + !S_ISDIR(prev_entry->st.st_mode)) + return 0; + + if ((error = filesystem_iterator_frame_push(iter, prev_entry)) < 0) + return error; + } + + /* we've advanced into the directory in question, let advance + * find the first entry + */ + return filesystem_iterator_advance(out, i); +} + +int git_iterator_current_workdir_path(git_str **out, git_iterator *i) +{ + filesystem_iterator *iter = GIT_CONTAINER_OF(i, filesystem_iterator, base); + const git_index_entry *entry; + + if (i->type != GIT_ITERATOR_FS && + i->type != GIT_ITERATOR_WORKDIR) { + *out = NULL; + return 0; + } + + git_str_truncate(&iter->current_path, iter->root_len); + + if (git_iterator_current(&entry, i) < 0 || + git_str_puts(&iter->current_path, entry->path) < 0) + return -1; + + *out = &iter->current_path; + return 0; +} + +GIT_INLINE(git_dir_flag) entry_dir_flag(git_index_entry *entry) +{ +#if defined(GIT_WIN32) && !defined(__MINGW32__) + return (entry && entry->mode) ? + (S_ISDIR(entry->mode) ? GIT_DIR_FLAG_TRUE : GIT_DIR_FLAG_FALSE) : + GIT_DIR_FLAG_UNKNOWN; +#else + GIT_UNUSED(entry); + return GIT_DIR_FLAG_UNKNOWN; +#endif +} + +static void filesystem_iterator_update_ignored(filesystem_iterator *iter) +{ + filesystem_iterator_frame *frame; + git_dir_flag dir_flag = entry_dir_flag(&iter->entry); + + if (git_ignore__lookup(&iter->current_is_ignored, + &iter->ignores, iter->entry.path, dir_flag) < 0) { + git_error_clear(); + iter->current_is_ignored = GIT_IGNORE_NOTFOUND; + } + + /* use ignore from containing frame stack */ + if (iter->current_is_ignored <= GIT_IGNORE_NOTFOUND) { + frame = filesystem_iterator_current_frame(iter); + iter->current_is_ignored = frame->is_ignored; + } +} + +GIT_INLINE(bool) filesystem_iterator_current_is_ignored( + filesystem_iterator *iter) +{ + if (iter->current_is_ignored == GIT_IGNORE_UNCHECKED) + filesystem_iterator_update_ignored(iter); + + return (iter->current_is_ignored == GIT_IGNORE_TRUE); +} + +bool git_iterator_current_is_ignored(git_iterator *i) +{ + filesystem_iterator *iter = NULL; + + if (i->type != GIT_ITERATOR_WORKDIR) + return false; + + iter = GIT_CONTAINER_OF(i, filesystem_iterator, base); + + return filesystem_iterator_current_is_ignored(iter); +} + +bool git_iterator_current_tree_is_ignored(git_iterator *i) +{ + filesystem_iterator *iter = GIT_CONTAINER_OF(i, filesystem_iterator, base); + filesystem_iterator_frame *frame; + + if (i->type != GIT_ITERATOR_WORKDIR) + return false; + + frame = filesystem_iterator_current_frame(iter); + return (frame->is_ignored == GIT_IGNORE_TRUE); +} + +static int filesystem_iterator_advance_over( + const git_index_entry **out, + git_iterator_status_t *status, + git_iterator *i) +{ + filesystem_iterator *iter = GIT_CONTAINER_OF(i, filesystem_iterator, base); + filesystem_iterator_frame *current_frame; + filesystem_iterator_entry *current_entry; + const git_index_entry *entry = NULL; + const char *base; + int error = 0; + + *out = NULL; + *status = GIT_ITERATOR_STATUS_NORMAL; + + GIT_ASSERT(iterator__has_been_accessed(i)); + + current_frame = filesystem_iterator_current_frame(iter); + GIT_ASSERT(current_frame); + + current_entry = filesystem_iterator_current_entry(current_frame); + GIT_ASSERT(current_entry); + + if ((error = git_iterator_current(&entry, i)) < 0) + return error; + + if (!S_ISDIR(entry->mode)) { + if (filesystem_iterator_current_is_ignored(iter)) + *status = GIT_ITERATOR_STATUS_IGNORED; + + return filesystem_iterator_advance(out, i); + } + + git_str_clear(&iter->tmp_buf); + if ((error = git_str_puts(&iter->tmp_buf, entry->path)) < 0) + return error; + + base = iter->tmp_buf.ptr; + + /* scan inside the directory looking for files. if we find nothing, + * we will remain EMPTY. if we find any ignored item, upgrade EMPTY to + * IGNORED. if we find a real actual item, upgrade all the way to NORMAL + * and then stop. + * + * however, if we're here looking for a pathlist item (but are not + * actually in the pathlist ourselves) then start at FILTERED instead of + * EMPTY. callers then know that this path was not something they asked + * about. + */ + *status = current_entry->match == ITERATOR_PATHLIST_IS_PARENT ? + GIT_ITERATOR_STATUS_FILTERED : GIT_ITERATOR_STATUS_EMPTY; + + while (entry && !iter->base.prefixcomp(entry->path, base)) { + if (filesystem_iterator_current_is_ignored(iter)) { + /* if we found an explicitly ignored item, then update from + * EMPTY to IGNORED + */ + *status = GIT_ITERATOR_STATUS_IGNORED; + } else if (S_ISDIR(entry->mode)) { + error = filesystem_iterator_advance_into(&entry, i); + + if (!error) + continue; + + /* this directory disappeared, ignore it */ + else if (error == GIT_ENOTFOUND) + error = 0; + + /* a real error occurred */ + else + break; + } else { + /* we found a non-ignored item, treat parent as untracked */ + *status = GIT_ITERATOR_STATUS_NORMAL; + break; + } + + if ((error = git_iterator_advance(&entry, i)) < 0) + break; + } + + /* wrap up scan back to base directory */ + while (entry && !iter->base.prefixcomp(entry->path, base)) { + if ((error = git_iterator_advance(&entry, i)) < 0) + break; + } + + if (!error) + *out = entry; + + return error; +} + +static void filesystem_iterator_clear(filesystem_iterator *iter) +{ + while (iter->frames.size) + filesystem_iterator_frame_pop(iter); + + git_array_clear(iter->frames); + git_ignore__free(&iter->ignores); + + git_str_dispose(&iter->tmp_buf); + + iterator_clear(&iter->base); +} + +static int filesystem_iterator_init(filesystem_iterator *iter) +{ + int error; + + if (iterator__honor_ignores(&iter->base) && + (error = git_ignore__for_path(iter->base.repo, + ".gitignore", &iter->ignores)) < 0) + return error; + + if ((error = filesystem_iterator_frame_push(iter, NULL)) < 0) + return error; + + iter->base.flags &= ~GIT_ITERATOR_FIRST_ACCESS; + + return 0; +} + +static int filesystem_iterator_reset(git_iterator *i) +{ + filesystem_iterator *iter = GIT_CONTAINER_OF(i, filesystem_iterator, base); + + filesystem_iterator_clear(iter); + return filesystem_iterator_init(iter); +} + +static void filesystem_iterator_free(git_iterator *i) +{ + filesystem_iterator *iter = GIT_CONTAINER_OF(i, filesystem_iterator, base); + git__free(iter->root); + git_str_dispose(&iter->current_path); + git_tree_free(iter->tree); + if (iter->index) + git_index_snapshot_release(&iter->index_snapshot, iter->index); + filesystem_iterator_clear(iter); +} + +static int iterator_for_filesystem( + git_iterator **out, + git_repository *repo, + const char *root, + git_index *index, + git_tree *tree, + git_iterator_t type, + git_iterator_options *options) +{ + filesystem_iterator *iter; + size_t root_len; + int error; + + static git_iterator_callbacks callbacks = { + filesystem_iterator_current, + filesystem_iterator_advance, + filesystem_iterator_advance_into, + filesystem_iterator_advance_over, + filesystem_iterator_reset, + filesystem_iterator_free + }; + + *out = NULL; + + if (root == NULL) + return git_iterator_for_nothing(out, options); + + iter = git__calloc(1, sizeof(filesystem_iterator)); + GIT_ERROR_CHECK_ALLOC(iter); + + iter->base.type = type; + iter->base.cb = &callbacks; + + root_len = strlen(root); + + iter->root = git__malloc(root_len+2); + GIT_ERROR_CHECK_ALLOC(iter->root); + + memcpy(iter->root, root, root_len); + + if (root_len == 0 || root[root_len-1] != '/') { + iter->root[root_len] = '/'; + root_len++; + } + iter->root[root_len] = '\0'; + iter->root_len = root_len; + + if ((error = git_str_puts(&iter->current_path, iter->root)) < 0) + goto on_error; + + if ((error = iterator_init_common(&iter->base, repo, index, options)) < 0) + goto on_error; + + if (tree && (error = git_tree_dup(&iter->tree, tree)) < 0) + goto on_error; + + if (index && + (error = git_index_snapshot_new(&iter->index_snapshot, index)) < 0) + goto on_error; + + iter->index = index; + iter->dirload_flags = + (iterator__ignore_case(&iter->base) ? + GIT_FS_PATH_DIR_IGNORE_CASE : 0) | + (iterator__flag(&iter->base, PRECOMPOSE_UNICODE) ? + GIT_FS_PATH_DIR_PRECOMPOSE_UNICODE : 0); + + iter->oid_type = options->oid_type; + + if ((error = filesystem_iterator_init(iter)) < 0) + goto on_error; + + *out = &iter->base; + return 0; + +on_error: + git_iterator_free(&iter->base); + return error; +} + +int git_iterator_for_filesystem( + git_iterator **out, + const char *root, + git_iterator_options *given_opts) +{ + git_iterator_options options = GIT_ITERATOR_OPTIONS_INIT; + + if (given_opts) + memcpy(&options, given_opts, sizeof(git_iterator_options)); + + return iterator_for_filesystem(out, + NULL, root, NULL, NULL, GIT_ITERATOR_FS, &options); +} + +int git_iterator_for_workdir_ext( + git_iterator **out, + git_repository *repo, + const char *repo_workdir, + git_index *index, + git_tree *tree, + git_iterator_options *given_opts) +{ + git_iterator_options options = GIT_ITERATOR_OPTIONS_INIT; + + if (!repo_workdir) { + if (git_repository__ensure_not_bare(repo, "scan working directory") < 0) + return GIT_EBAREREPO; + + repo_workdir = git_repository_workdir(repo); + } + + /* upgrade to a workdir iterator, adding necessary internal flags */ + if (given_opts) + memcpy(&options, given_opts, sizeof(git_iterator_options)); + + options.flags |= GIT_ITERATOR_HONOR_IGNORES | + GIT_ITERATOR_IGNORE_DOT_GIT; + + if (!options.oid_type) + options.oid_type = repo->oid_type; + else if (options.oid_type != repo->oid_type) + git_error_set(GIT_ERROR_INVALID, + "specified object ID type does not match repository object ID type"); + + return iterator_for_filesystem(out, + repo, repo_workdir, index, tree, GIT_ITERATOR_WORKDIR, &options); +} + + +/* Index iterator */ + + +typedef struct { + git_iterator base; + git_vector entries; + size_t next_idx; + + /* the pseudotree entry */ + git_index_entry tree_entry; + git_str tree_buf; + bool skip_tree; + + const git_index_entry *entry; +} index_iterator; + +static int index_iterator_current( + const git_index_entry **out, git_iterator *i) +{ + index_iterator *iter = (index_iterator *)i; + + if (!iterator__has_been_accessed(i)) + return iter->base.cb->advance(out, i); + + if (iter->entry == NULL) { + *out = NULL; + return GIT_ITEROVER; + } + + *out = iter->entry; + return 0; +} + +static bool index_iterator_create_pseudotree( + const git_index_entry **out, + index_iterator *iter, + const char *path) +{ + const char *prev_path, *relative_path, *dirsep; + size_t common_len; + + prev_path = iter->entry ? iter->entry->path : ""; + + /* determine if the new path is in a different directory from the old */ + common_len = git_fs_path_common_dirlen(prev_path, path); + relative_path = path + common_len; + + if ((dirsep = strchr(relative_path, '/')) == NULL) + return false; + + git_str_clear(&iter->tree_buf); + git_str_put(&iter->tree_buf, path, (dirsep - path) + 1); + + iter->tree_entry.mode = GIT_FILEMODE_TREE; + iter->tree_entry.path = iter->tree_buf.ptr; + + *out = &iter->tree_entry; + return true; +} + +static int index_iterator_skip_pseudotree(index_iterator *iter) +{ + GIT_ASSERT(iterator__has_been_accessed(&iter->base)); + GIT_ASSERT(S_ISDIR(iter->entry->mode)); + + while (true) { + const git_index_entry *next_entry = NULL; + + if (++iter->next_idx >= iter->entries.length) + return GIT_ITEROVER; + + next_entry = iter->entries.contents[iter->next_idx]; + + if (iter->base.strncomp(iter->tree_buf.ptr, next_entry->path, + iter->tree_buf.size) != 0) + break; + } + + iter->skip_tree = false; + return 0; +} + +static int index_iterator_advance( + const git_index_entry **out, git_iterator *i) +{ + index_iterator *iter = GIT_CONTAINER_OF(i, index_iterator, base); + const git_index_entry *entry = NULL; + bool is_submodule; + int error = 0; + + iter->base.flags |= GIT_ITERATOR_FIRST_ACCESS; + + while (true) { + if (iter->next_idx >= iter->entries.length) { + error = GIT_ITEROVER; + break; + } + + /* we were not asked to expand this pseudotree. advance over it. */ + if (iter->skip_tree) { + index_iterator_skip_pseudotree(iter); + continue; + } + + entry = iter->entries.contents[iter->next_idx]; + is_submodule = S_ISGITLINK(entry->mode); + + if (!iterator_has_started(&iter->base, entry->path, is_submodule)) { + iter->next_idx++; + continue; + } + + if (iterator_has_ended(&iter->base, entry->path)) { + error = GIT_ITEROVER; + break; + } + + /* if we have a list of paths we're interested in, examine it */ + if (!iterator_pathlist_next_is(&iter->base, entry->path)) { + iter->next_idx++; + continue; + } + + /* if this is a conflict, skip it unless we're including conflicts */ + if (git_index_entry_is_conflict(entry) && + !iterator__include_conflicts(&iter->base)) { + iter->next_idx++; + continue; + } + + /* we've found what will be our next _file_ entry. but if we are + * returning trees entries, we may need to return a pseudotree + * entry that will contain this. don't advance over this entry, + * though, we still need to return it on the next `advance`. + */ + if (iterator__include_trees(&iter->base) && + index_iterator_create_pseudotree(&entry, iter, entry->path)) { + + /* Note whether this pseudo tree should be expanded or not */ + iter->skip_tree = iterator__dont_autoexpand(&iter->base); + break; + } + + iter->next_idx++; + break; + } + + iter->entry = (error == 0) ? entry : NULL; + + if (out) + *out = iter->entry; + + return error; +} + +static int index_iterator_advance_into( + const git_index_entry **out, git_iterator *i) +{ + index_iterator *iter = GIT_CONTAINER_OF(i, index_iterator, base); + + if (! S_ISDIR(iter->tree_entry.mode)) { + if (out) + *out = NULL; + + return 0; + } + + iter->skip_tree = false; + return index_iterator_advance(out, i); +} + +static int index_iterator_advance_over( + const git_index_entry **out, + git_iterator_status_t *status, + git_iterator *i) +{ + index_iterator *iter = GIT_CONTAINER_OF(i, index_iterator, base); + const git_index_entry *entry; + int error; + + if ((error = index_iterator_current(&entry, i)) < 0) + return error; + + if (S_ISDIR(entry->mode)) + index_iterator_skip_pseudotree(iter); + + *status = GIT_ITERATOR_STATUS_NORMAL; + return index_iterator_advance(out, i); +} + +static void index_iterator_clear(index_iterator *iter) +{ + iterator_clear(&iter->base); +} + +static int index_iterator_init(index_iterator *iter) +{ + iter->base.flags &= ~GIT_ITERATOR_FIRST_ACCESS; + iter->next_idx = 0; + iter->skip_tree = false; + return 0; +} + +static int index_iterator_reset(git_iterator *i) +{ + index_iterator *iter = GIT_CONTAINER_OF(i, index_iterator, base); + + index_iterator_clear(iter); + return index_iterator_init(iter); +} + +static void index_iterator_free(git_iterator *i) +{ + index_iterator *iter = GIT_CONTAINER_OF(i, index_iterator, base); + + git_index_snapshot_release(&iter->entries, iter->base.index); + git_str_dispose(&iter->tree_buf); +} + +int git_iterator_for_index( + git_iterator **out, + git_repository *repo, + git_index *index, + git_iterator_options *options) +{ + index_iterator *iter; + int error; + + static git_iterator_callbacks callbacks = { + index_iterator_current, + index_iterator_advance, + index_iterator_advance_into, + index_iterator_advance_over, + index_iterator_reset, + index_iterator_free + }; + + *out = NULL; + + if (index == NULL) + return git_iterator_for_nothing(out, options); + + iter = git__calloc(1, sizeof(index_iterator)); + GIT_ERROR_CHECK_ALLOC(iter); + + iter->base.type = GIT_ITERATOR_INDEX; + iter->base.cb = &callbacks; + + if ((error = iterator_init_common(&iter->base, repo, index, options)) < 0 || + (error = git_index_snapshot_new(&iter->entries, index)) < 0 || + (error = index_iterator_init(iter)) < 0) + goto on_error; + + git_vector_set_cmp(&iter->entries, iterator__ignore_case(&iter->base) ? + git_index_entry_icmp : git_index_entry_cmp); + git_vector_sort(&iter->entries); + + *out = &iter->base; + return 0; + +on_error: + git_iterator_free(&iter->base); + return error; +} + + +/* Iterator API */ + +int git_iterator_reset_range( + git_iterator *i, const char *start, const char *end) +{ + if (iterator_reset_range(i, start, end) < 0) + return -1; + + return i->cb->reset(i); +} + +int git_iterator_set_ignore_case(git_iterator *i, bool ignore_case) +{ + GIT_ASSERT(!iterator__has_been_accessed(i)); + iterator_set_ignore_case(i, ignore_case); + return 0; +} + +void git_iterator_free(git_iterator *iter) +{ + if (iter == NULL) + return; + + iter->cb->free(iter); + + git_vector_free(&iter->pathlist); + git__free(iter->start); + git__free(iter->end); + + memset(iter, 0, sizeof(*iter)); + + git__free(iter); +} + +int git_iterator_foreach( + git_iterator *iterator, + git_iterator_foreach_cb cb, + void *data) +{ + const git_index_entry *iterator_item; + int error = 0; + + if ((error = git_iterator_current(&iterator_item, iterator)) < 0) + goto done; + + if ((error = cb(iterator_item, data)) != 0) + goto done; + + while (true) { + if ((error = git_iterator_advance(&iterator_item, iterator)) < 0) + goto done; + + if ((error = cb(iterator_item, data)) != 0) + goto done; + } + +done: + if (error == GIT_ITEROVER) + error = 0; + + return error; +} + +int git_iterator_walk( + git_iterator **iterators, + size_t cnt, + git_iterator_walk_cb cb, + void *data) +{ + const git_index_entry **iterator_item; /* next in each iterator */ + const git_index_entry **cur_items; /* current path in each iter */ + const git_index_entry *first_match; + size_t i, j; + int error = 0; + + iterator_item = git__calloc(cnt, sizeof(git_index_entry *)); + cur_items = git__calloc(cnt, sizeof(git_index_entry *)); + + GIT_ERROR_CHECK_ALLOC(iterator_item); + GIT_ERROR_CHECK_ALLOC(cur_items); + + /* Set up the iterators */ + for (i = 0; i < cnt; i++) { + error = git_iterator_current(&iterator_item[i], iterators[i]); + + if (error < 0 && error != GIT_ITEROVER) + goto done; + } + + while (true) { + for (i = 0; i < cnt; i++) + cur_items[i] = NULL; + + first_match = NULL; + + /* Find the next path(s) to consume from each iterator */ + for (i = 0; i < cnt; i++) { + if (iterator_item[i] == NULL) + continue; + + if (first_match == NULL) { + first_match = iterator_item[i]; + cur_items[i] = iterator_item[i]; + } else { + int path_diff = git_index_entry_cmp(iterator_item[i], first_match); + + if (path_diff < 0) { + /* Found an index entry that sorts before the one we're + * looking at. Forget that we've seen the other and + * look at the other iterators for this path. + */ + for (j = 0; j < i; j++) + cur_items[j] = NULL; + + first_match = iterator_item[i]; + cur_items[i] = iterator_item[i]; + } else if (path_diff == 0) { + cur_items[i] = iterator_item[i]; + } + } + } + + if (first_match == NULL) + break; + + if ((error = cb(cur_items, data)) != 0) + goto done; + + /* Advance each iterator that participated */ + for (i = 0; i < cnt; i++) { + if (cur_items[i] == NULL) + continue; + + error = git_iterator_advance(&iterator_item[i], iterators[i]); + + if (error < 0 && error != GIT_ITEROVER) + goto done; + } + } + +done: + git__free((git_index_entry **)iterator_item); + git__free((git_index_entry **)cur_items); + + if (error == GIT_ITEROVER) + error = 0; + + return error; +} diff --git a/src/libgit2/iterator.h b/src/libgit2/iterator.h new file mode 100644 index 0000000..7963ce7 --- /dev/null +++ b/src/libgit2/iterator.h @@ -0,0 +1,325 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_iterator_h__ +#define INCLUDE_iterator_h__ + +#include "common.h" + +#include "git2/index.h" +#include "vector.h" +#include "str.h" +#include "ignore.h" + +typedef struct git_iterator git_iterator; + +typedef enum { + GIT_ITERATOR_EMPTY = 0, + GIT_ITERATOR_TREE = 1, + GIT_ITERATOR_INDEX = 2, + GIT_ITERATOR_WORKDIR = 3, + GIT_ITERATOR_FS = 4 +} git_iterator_t; + +typedef enum { + /** ignore case for entry sort order */ + GIT_ITERATOR_IGNORE_CASE = (1u << 0), + /** force case sensitivity for entry sort order */ + GIT_ITERATOR_DONT_IGNORE_CASE = (1u << 1), + /** return tree items in addition to blob items */ + GIT_ITERATOR_INCLUDE_TREES = (1u << 2), + /** don't flatten trees, requiring advance_into (implies INCLUDE_TREES) */ + GIT_ITERATOR_DONT_AUTOEXPAND = (1u << 3), + /** convert precomposed unicode to decomposed unicode */ + GIT_ITERATOR_PRECOMPOSE_UNICODE = (1u << 4), + /** never convert precomposed unicode to decomposed unicode */ + GIT_ITERATOR_DONT_PRECOMPOSE_UNICODE = (1u << 5), + /** include conflicts */ + GIT_ITERATOR_INCLUDE_CONFLICTS = (1u << 6), + /** descend into symlinked directories */ + GIT_ITERATOR_DESCEND_SYMLINKS = (1u << 7), + /** hash files in workdir or filesystem iterators */ + GIT_ITERATOR_INCLUDE_HASH = (1u << 8) +} git_iterator_flag_t; + +typedef enum { + GIT_ITERATOR_STATUS_NORMAL = 0, + GIT_ITERATOR_STATUS_IGNORED = 1, + GIT_ITERATOR_STATUS_EMPTY = 2, + GIT_ITERATOR_STATUS_FILTERED = 3 +} git_iterator_status_t; + +typedef struct { + const char *start; + const char *end; + + /* paths to include in the iterator (literal). if set, any paths not + * listed here will be excluded from iteration. + */ + git_strarray pathlist; + + /* flags, from above */ + unsigned int flags; + + /* oid type - necessary for non-workdir filesystem iterators */ + git_oid_t oid_type; +} git_iterator_options; + +#define GIT_ITERATOR_OPTIONS_INIT {0} + +typedef struct { + int (*current)(const git_index_entry **, git_iterator *); + int (*advance)(const git_index_entry **, git_iterator *); + int (*advance_into)(const git_index_entry **, git_iterator *); + int (*advance_over)( + const git_index_entry **, git_iterator_status_t *, git_iterator *); + int (*reset)(git_iterator *); + void (*free)(git_iterator *); +} git_iterator_callbacks; + +struct git_iterator { + git_iterator_t type; + git_iterator_callbacks *cb; + + git_repository *repo; + git_index *index; + + char *start; + size_t start_len; + + char *end; + size_t end_len; + + bool started; + bool ended; + git_vector pathlist; + size_t pathlist_walk_idx; + int (*strcomp)(const char *a, const char *b); + int (*strncomp)(const char *a, const char *b, size_t n); + int (*prefixcomp)(const char *str, const char *prefix); + int (*entry_srch)(const void *key, const void *array_member); + size_t stat_calls; + unsigned int flags; +}; + +extern int git_iterator_for_nothing( + git_iterator **out, + git_iterator_options *options); + +/* tree iterators will match the ignore_case value from the index of the + * repository, unless you override with a non-zero flag value + */ +extern int git_iterator_for_tree( + git_iterator **out, + git_tree *tree, + git_iterator_options *options); + +/* index iterators will take the ignore_case value from the index; the + * ignore_case flags are not used + */ +extern int git_iterator_for_index( + git_iterator **out, + git_repository *repo, + git_index *index, + git_iterator_options *options); + +extern int git_iterator_for_workdir_ext( + git_iterator **out, + git_repository *repo, + const char *repo_workdir, + git_index *index, + git_tree *tree, + git_iterator_options *options); + +/* workdir iterators will match the ignore_case value from the index of the + * repository, unless you override with a non-zero flag value + */ +GIT_INLINE(int) git_iterator_for_workdir( + git_iterator **out, + git_repository *repo, + git_index *index, + git_tree *tree, + git_iterator_options *options) +{ + return git_iterator_for_workdir_ext(out, repo, NULL, index, tree, options); +} + +/* for filesystem iterators, you have to explicitly pass in the ignore_case + * behavior that you desire + */ +extern int git_iterator_for_filesystem( + git_iterator **out, + const char *root, + git_iterator_options *options); + +extern void git_iterator_free(git_iterator *iter); + +/* Return a git_index_entry structure for the current value the iterator + * is looking at or NULL if the iterator is at the end. + * + * The entry may noy be fully populated. Tree iterators will only have a + * value mode, OID, and path. Workdir iterators will not have an OID (but + * you can use `git_iterator_current_oid()` to calculate it on demand). + * + * You do not need to free the entry. It is still "owned" by the iterator. + * Once you call `git_iterator_advance()` then the old entry is no longer + * guaranteed to be valid - it may be freed or just overwritten in place. + */ +GIT_INLINE(int) git_iterator_current( + const git_index_entry **entry, git_iterator *iter) +{ + return iter->cb->current(entry, iter); +} + +/** + * Advance to the next item for the iterator. + * + * If GIT_ITERATOR_INCLUDE_TREES is set, this may be a tree item. If + * GIT_ITERATOR_DONT_AUTOEXPAND is set, calling this again when on a tree + * item will skip over all the items under that tree. + */ +GIT_INLINE(int) git_iterator_advance( + const git_index_entry **entry, git_iterator *iter) +{ + return iter->cb->advance(entry, iter); +} + +/** + * Iterate into a tree item (when GIT_ITERATOR_DONT_AUTOEXPAND is set). + * + * git_iterator_advance() steps through all items being iterated over + * (either with or without trees, depending on GIT_ITERATOR_INCLUDE_TREES), + * but if GIT_ITERATOR_DONT_AUTOEXPAND is set, it will skip to the next + * sibling of a tree instead of going to the first child of the tree. In + * that case, use this function to advance to the first child of the tree. + * + * If the current item is not a tree, this is a no-op. + * + * For filesystem and working directory iterators, a tree (i.e. directory) + * can be empty. In that case, this function returns GIT_ENOTFOUND and + * does not advance. That can't happen for tree and index iterators. + */ +GIT_INLINE(int) git_iterator_advance_into( + const git_index_entry **entry, git_iterator *iter) +{ + return iter->cb->advance_into(entry, iter); +} + +/* Advance over a directory and check if it contains no files or just + * ignored files. + * + * In a tree or the index, all directories will contain files, but in the + * working directory it is possible to have an empty directory tree or a + * tree that only contains ignored files. Many Git operations treat these + * cases specially. This advances over a directory (presumably an + * untracked directory) but checks during the scan if there are any files + * and any non-ignored files. + */ +GIT_INLINE(int) git_iterator_advance_over( + const git_index_entry **entry, + git_iterator_status_t *status, + git_iterator *iter) +{ + return iter->cb->advance_over(entry, status, iter); +} + +/** + * Go back to the start of the iteration. + */ +GIT_INLINE(int) git_iterator_reset(git_iterator *iter) +{ + return iter->cb->reset(iter); +} + +/** + * Go back to the start of the iteration after updating the `start` and + * `end` pathname boundaries of the iteration. + */ +extern int git_iterator_reset_range( + git_iterator *iter, const char *start, const char *end); + +GIT_INLINE(git_iterator_t) git_iterator_type(git_iterator *iter) +{ + return iter->type; +} + +GIT_INLINE(git_repository *) git_iterator_owner(git_iterator *iter) +{ + return iter->repo; +} + +GIT_INLINE(git_index *) git_iterator_index(git_iterator *iter) +{ + return iter->index; +} + +GIT_INLINE(git_iterator_flag_t) git_iterator_flags(git_iterator *iter) +{ + return iter->flags; +} + +GIT_INLINE(bool) git_iterator_ignore_case(git_iterator *iter) +{ + return ((iter->flags & GIT_ITERATOR_IGNORE_CASE) != 0); +} + +extern int git_iterator_set_ignore_case( + git_iterator *iter, bool ignore_case); + +extern int git_iterator_current_tree_entry( + const git_tree_entry **entry_out, git_iterator *iter); + +extern int git_iterator_current_parent_tree( + const git_tree **tree_out, git_iterator *iter, size_t depth); + +extern bool git_iterator_current_is_ignored(git_iterator *iter); + +extern bool git_iterator_current_tree_is_ignored(git_iterator *iter); + +/** + * Get full path of the current item from a workdir iterator. This will + * return NULL for a non-workdir iterator. The git_str is still owned by + * the iterator; this is exposed just for efficiency. + */ +extern int git_iterator_current_workdir_path( + git_str **path, git_iterator *iter); + +/** + * Retrieve the index stored in the iterator. + * + * Only implemented for the workdir and index iterators. + */ +extern git_index *git_iterator_index(git_iterator *iter); + +typedef int (*git_iterator_foreach_cb)( + const git_index_entry *entry, + void *data); + +/** + * Walk the given iterator and invoke the callback for each path + * contained in the iterator. + */ +extern int git_iterator_foreach( + git_iterator *iterator, + git_iterator_foreach_cb cb, + void *data); + +typedef int (*git_iterator_walk_cb)( + const git_index_entry **entries, + void *data); + +/** + * Walk the given iterators in lock-step. The given callback will be + * called for each unique path, with the index entry in each iterator + * (or NULL if the given iterator does not contain that path). + */ +extern int git_iterator_walk( + git_iterator **iterators, + size_t cnt, + git_iterator_walk_cb cb, + void *data); + +#endif diff --git a/src/libgit2/libgit2.c b/src/libgit2/libgit2.c new file mode 100644 index 0000000..ce28714 --- /dev/null +++ b/src/libgit2/libgit2.c @@ -0,0 +1,483 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "libgit2.h" + +#include +#include "alloc.h" +#include "buf.h" +#include "cache.h" +#include "common.h" +#include "filter.h" +#include "grafts.h" +#include "hash.h" +#include "index.h" +#include "merge_driver.h" +#include "pool.h" +#include "mwindow.h" +#include "object.h" +#include "odb.h" +#include "rand.h" +#include "refs.h" +#include "runtime.h" +#include "sysdir.h" +#include "thread.h" +#include "threadstate.h" +#include "git2/global.h" +#include "streams/registry.h" +#include "streams/mbedtls.h" +#include "streams/openssl.h" +#include "streams/socket.h" +#include "transports/smart.h" +#include "transports/http.h" +#include "transports/ssh.h" + +#ifdef GIT_WIN32 +# include "win32/w32_leakcheck.h" +#endif + +/* Declarations for tuneable settings */ +extern size_t git_mwindow__window_size; +extern size_t git_mwindow__mapped_limit; +extern size_t git_mwindow__file_limit; +extern size_t git_indexer__max_objects; +extern bool git_disable_pack_keep_file_checks; +extern int git_odb__packed_priority; +extern int git_odb__loose_priority; +extern int git_socket_stream__connect_timeout; +extern int git_socket_stream__timeout; + +char *git__user_agent; +char *git__ssl_ciphers; + +static void libgit2_settings_global_shutdown(void) +{ + git__free(git__user_agent); + git__free(git__ssl_ciphers); + git_repository__free_extensions(); +} + +static int git_libgit2_settings_global_init(void) +{ + return git_runtime_shutdown_register(libgit2_settings_global_shutdown); +} + +int git_libgit2_init(void) +{ + static git_runtime_init_fn init_fns[] = { +#ifdef GIT_WIN32 + git_win32_leakcheck_global_init, +#endif + git_allocator_global_init, + git_threadstate_global_init, + git_threads_global_init, + git_rand_global_init, + git_hash_global_init, + git_sysdir_global_init, + git_filter_global_init, + git_merge_driver_global_init, + git_transport_ssh_global_init, + git_stream_registry_global_init, + git_socket_stream_global_init, + git_openssl_stream_global_init, + git_mbedtls_stream_global_init, + git_mwindow_global_init, + git_pool_global_init, + git_libgit2_settings_global_init + }; + + return git_runtime_init(init_fns, ARRAY_SIZE(init_fns)); +} + +int git_libgit2_init_count(void) +{ + return git_runtime_init_count(); +} + +int git_libgit2_shutdown(void) +{ + return git_runtime_shutdown(); +} + +int git_libgit2_version(int *major, int *minor, int *rev) +{ + *major = LIBGIT2_VER_MAJOR; + *minor = LIBGIT2_VER_MINOR; + *rev = LIBGIT2_VER_REVISION; + + return 0; +} + +const char *git_libgit2_prerelease(void) +{ + return LIBGIT2_VER_PRERELEASE; +} + +int git_libgit2_features(void) +{ + return 0 +#ifdef GIT_THREADS + | GIT_FEATURE_THREADS +#endif +#ifdef GIT_HTTPS + | GIT_FEATURE_HTTPS +#endif +#if defined(GIT_SSH) + | GIT_FEATURE_SSH +#endif +#if defined(GIT_USE_NSEC) + | GIT_FEATURE_NSEC +#endif + ; +} + +static int config_level_to_sysdir(int *out, int config_level) +{ + switch (config_level) { + case GIT_CONFIG_LEVEL_SYSTEM: + *out = GIT_SYSDIR_SYSTEM; + return 0; + case GIT_CONFIG_LEVEL_XDG: + *out = GIT_SYSDIR_XDG; + return 0; + case GIT_CONFIG_LEVEL_GLOBAL: + *out = GIT_SYSDIR_GLOBAL; + return 0; + case GIT_CONFIG_LEVEL_PROGRAMDATA: + *out = GIT_SYSDIR_PROGRAMDATA; + return 0; + default: + break; + } + + git_error_set( + GIT_ERROR_INVALID, "invalid config path selector %d", config_level); + return -1; +} + +const char *git_libgit2__user_agent(void) +{ + return git__user_agent; +} + +const char *git_libgit2__ssl_ciphers(void) +{ + return git__ssl_ciphers; +} + +int git_libgit2_opts(int key, ...) +{ + int error = 0; + va_list ap; + + va_start(ap, key); + + switch (key) { + case GIT_OPT_SET_MWINDOW_SIZE: + git_mwindow__window_size = va_arg(ap, size_t); + break; + + case GIT_OPT_GET_MWINDOW_SIZE: + *(va_arg(ap, size_t *)) = git_mwindow__window_size; + break; + + case GIT_OPT_SET_MWINDOW_MAPPED_LIMIT: + git_mwindow__mapped_limit = va_arg(ap, size_t); + break; + + case GIT_OPT_GET_MWINDOW_MAPPED_LIMIT: + *(va_arg(ap, size_t *)) = git_mwindow__mapped_limit; + break; + + case GIT_OPT_SET_MWINDOW_FILE_LIMIT: + git_mwindow__file_limit = va_arg(ap, size_t); + break; + + case GIT_OPT_GET_MWINDOW_FILE_LIMIT: + *(va_arg(ap, size_t *)) = git_mwindow__file_limit; + break; + + case GIT_OPT_GET_SEARCH_PATH: + { + int sysdir = va_arg(ap, int); + git_buf *out = va_arg(ap, git_buf *); + git_str str = GIT_STR_INIT; + const git_str *tmp; + int level; + + if ((error = git_buf_tostr(&str, out)) < 0 || + (error = config_level_to_sysdir(&level, sysdir)) < 0 || + (error = git_sysdir_get(&tmp, level)) < 0 || + (error = git_str_put(&str, tmp->ptr, tmp->size)) < 0) + break; + + error = git_buf_fromstr(out, &str); + } + break; + + case GIT_OPT_SET_SEARCH_PATH: + { + int level; + + if ((error = config_level_to_sysdir(&level, va_arg(ap, int))) >= 0) + error = git_sysdir_set(level, va_arg(ap, const char *)); + } + break; + + case GIT_OPT_SET_CACHE_OBJECT_LIMIT: + { + git_object_t type = (git_object_t)va_arg(ap, int); + size_t size = va_arg(ap, size_t); + error = git_cache_set_max_object_size(type, size); + break; + } + + case GIT_OPT_SET_CACHE_MAX_SIZE: + git_cache__max_storage = va_arg(ap, ssize_t); + break; + + case GIT_OPT_ENABLE_CACHING: + git_cache__enabled = (va_arg(ap, int) != 0); + break; + + case GIT_OPT_GET_CACHED_MEMORY: + *(va_arg(ap, ssize_t *)) = git_cache__current_storage.val; + *(va_arg(ap, ssize_t *)) = git_cache__max_storage; + break; + + case GIT_OPT_GET_TEMPLATE_PATH: + { + git_buf *out = va_arg(ap, git_buf *); + git_str str = GIT_STR_INIT; + const git_str *tmp; + + if ((error = git_buf_tostr(&str, out)) < 0 || + (error = git_sysdir_get(&tmp, GIT_SYSDIR_TEMPLATE)) < 0 || + (error = git_str_put(&str, tmp->ptr, tmp->size)) < 0) + break; + + error = git_buf_fromstr(out, &str); + } + break; + + case GIT_OPT_SET_TEMPLATE_PATH: + error = git_sysdir_set(GIT_SYSDIR_TEMPLATE, va_arg(ap, const char *)); + break; + + case GIT_OPT_SET_SSL_CERT_LOCATIONS: +#ifdef GIT_OPENSSL + { + const char *file = va_arg(ap, const char *); + const char *path = va_arg(ap, const char *); + error = git_openssl__set_cert_location(file, path); + } +#elif defined(GIT_MBEDTLS) + { + const char *file = va_arg(ap, const char *); + const char *path = va_arg(ap, const char *); + error = git_mbedtls__set_cert_location(file, path); + } +#else + git_error_set(GIT_ERROR_SSL, "TLS backend doesn't support certificate locations"); + error = -1; +#endif + break; + case GIT_OPT_SET_USER_AGENT: + git__free(git__user_agent); + git__user_agent = git__strdup(va_arg(ap, const char *)); + if (!git__user_agent) { + git_error_set_oom(); + error = -1; + } + + break; + + case GIT_OPT_ENABLE_STRICT_OBJECT_CREATION: + git_object__strict_input_validation = (va_arg(ap, int) != 0); + break; + + case GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION: + git_reference__enable_symbolic_ref_target_validation = (va_arg(ap, int) != 0); + break; + + case GIT_OPT_SET_SSL_CIPHERS: +#if (GIT_OPENSSL || GIT_MBEDTLS) + { + git__free(git__ssl_ciphers); + git__ssl_ciphers = git__strdup(va_arg(ap, const char *)); + if (!git__ssl_ciphers) { + git_error_set_oom(); + error = -1; + } + } +#else + git_error_set(GIT_ERROR_SSL, "TLS backend doesn't support custom ciphers"); + error = -1; +#endif + break; + + case GIT_OPT_GET_USER_AGENT: + { + git_buf *out = va_arg(ap, git_buf *); + git_str str = GIT_STR_INIT; + + if ((error = git_buf_tostr(&str, out)) < 0 || + (error = git_str_puts(&str, git__user_agent)) < 0) + break; + + error = git_buf_fromstr(out, &str); + } + break; + + case GIT_OPT_ENABLE_OFS_DELTA: + git_smart__ofs_delta_enabled = (va_arg(ap, int) != 0); + break; + + case GIT_OPT_ENABLE_FSYNC_GITDIR: + git_repository__fsync_gitdir = (va_arg(ap, int) != 0); + break; + + case GIT_OPT_GET_WINDOWS_SHAREMODE: +#ifdef GIT_WIN32 + *(va_arg(ap, unsigned long *)) = git_win32__createfile_sharemode; +#endif + break; + + case GIT_OPT_SET_WINDOWS_SHAREMODE: +#ifdef GIT_WIN32 + git_win32__createfile_sharemode = va_arg(ap, unsigned long); +#endif + break; + + case GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION: + git_odb__strict_hash_verification = (va_arg(ap, int) != 0); + break; + + case GIT_OPT_SET_ALLOCATOR: + error = git_allocator_setup(va_arg(ap, git_allocator *)); + break; + + case GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY: + git_index__enforce_unsaved_safety = (va_arg(ap, int) != 0); + break; + + case GIT_OPT_SET_PACK_MAX_OBJECTS: + git_indexer__max_objects = va_arg(ap, size_t); + break; + + case GIT_OPT_GET_PACK_MAX_OBJECTS: + *(va_arg(ap, size_t *)) = git_indexer__max_objects; + break; + + case GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS: + git_disable_pack_keep_file_checks = (va_arg(ap, int) != 0); + break; + + case GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE: + git_http__expect_continue = (va_arg(ap, int) != 0); + break; + + case GIT_OPT_SET_ODB_PACKED_PRIORITY: + git_odb__packed_priority = va_arg(ap, int); + break; + + case GIT_OPT_SET_ODB_LOOSE_PRIORITY: + git_odb__loose_priority = va_arg(ap, int); + break; + + case GIT_OPT_SET_EXTENSIONS: + { + const char **extensions = va_arg(ap, const char **); + size_t len = va_arg(ap, size_t); + error = git_repository__set_extensions(extensions, len); + } + break; + + case GIT_OPT_GET_EXTENSIONS: + { + git_strarray *out = va_arg(ap, git_strarray *); + char **extensions; + size_t len; + + if ((error = git_repository__extensions(&extensions, &len)) < 0) + break; + + out->strings = extensions; + out->count = len; + } + break; + + case GIT_OPT_GET_OWNER_VALIDATION: + *(va_arg(ap, int *)) = git_repository__validate_ownership; + break; + + case GIT_OPT_SET_OWNER_VALIDATION: + git_repository__validate_ownership = (va_arg(ap, int) != 0); + break; + + case GIT_OPT_GET_HOMEDIR: + { + git_buf *out = va_arg(ap, git_buf *); + git_str str = GIT_STR_INIT; + const git_str *tmp; + + if ((error = git_buf_tostr(&str, out)) < 0 || + (error = git_sysdir_get(&tmp, GIT_SYSDIR_HOME)) < 0 || + (error = git_str_put(&str, tmp->ptr, tmp->size)) < 0) + break; + + error = git_buf_fromstr(out, &str); + } + break; + + case GIT_OPT_SET_HOMEDIR: + error = git_sysdir_set(GIT_SYSDIR_HOME, va_arg(ap, const char *)); + break; + + case GIT_OPT_GET_SERVER_CONNECT_TIMEOUT: + *(va_arg(ap, int *)) = git_socket_stream__connect_timeout; + break; + + case GIT_OPT_SET_SERVER_CONNECT_TIMEOUT: + { + int timeout = va_arg(ap, int); + + if (timeout < 0) { + git_error_set(GIT_ERROR_INVALID, "invalid connect timeout"); + error = -1; + } else { + git_socket_stream__connect_timeout = timeout; + } + } + break; + + case GIT_OPT_GET_SERVER_TIMEOUT: + *(va_arg(ap, int *)) = git_socket_stream__timeout; + break; + + case GIT_OPT_SET_SERVER_TIMEOUT: + { + int timeout = va_arg(ap, int); + + if (timeout < 0) { + git_error_set(GIT_ERROR_INVALID, "invalid timeout"); + error = -1; + } else { + git_socket_stream__timeout = timeout; + } + } + break; + + default: + git_error_set(GIT_ERROR_INVALID, "invalid option key"); + error = -1; + } + + va_end(ap); + + return error; +} diff --git a/src/libgit2/libgit2.h b/src/libgit2/libgit2.h new file mode 100644 index 0000000..a898367 --- /dev/null +++ b/src/libgit2/libgit2.h @@ -0,0 +1,15 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_libgit2_h__ +#define INCLUDE_libgit2_h__ + +extern int git_libgit2_init_count(void); + +extern const char *git_libgit2__user_agent(void); +extern const char *git_libgit2__ssl_ciphers(void); + +#endif diff --git a/src/libgit2/mailmap.c b/src/libgit2/mailmap.c new file mode 100644 index 0000000..4336fe3 --- /dev/null +++ b/src/libgit2/mailmap.c @@ -0,0 +1,500 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "mailmap.h" + +#include "common.h" +#include "config.h" +#include "fs_path.h" +#include "repository.h" +#include "signature.h" +#include "git2/config.h" +#include "git2/revparse.h" +#include "blob.h" +#include "parse.h" +#include "path.h" + +#define MM_FILE ".mailmap" +#define MM_FILE_CONFIG "mailmap.file" +#define MM_BLOB_CONFIG "mailmap.blob" +#define MM_BLOB_DEFAULT "HEAD:" MM_FILE + +static void mailmap_entry_free(git_mailmap_entry *entry) +{ + if (!entry) + return; + + git__free(entry->real_name); + git__free(entry->real_email); + git__free(entry->replace_name); + git__free(entry->replace_email); + git__free(entry); +} + +/* + * First we sort by replace_email, then replace_name (if present). + * Entries with names are greater than entries without. + */ +static int mailmap_entry_cmp(const void *a_raw, const void *b_raw) +{ + const git_mailmap_entry *a = (const git_mailmap_entry *)a_raw; + const git_mailmap_entry *b = (const git_mailmap_entry *)b_raw; + int cmp; + + GIT_ASSERT_ARG(a && a->replace_email); + GIT_ASSERT_ARG(b && b->replace_email); + + cmp = git__strcmp(a->replace_email, b->replace_email); + if (cmp) + return cmp; + + /* NULL replace_names are less than not-NULL ones */ + if (a->replace_name == NULL || b->replace_name == NULL) + return (int)(a->replace_name != NULL) - (int)(b->replace_name != NULL); + + return git__strcmp(a->replace_name, b->replace_name); +} + +/* Replace the old entry with the new on duplicate. */ +static int mailmap_entry_replace(void **old_raw, void *new_raw) +{ + mailmap_entry_free((git_mailmap_entry *)*old_raw); + *old_raw = new_raw; + return GIT_EEXISTS; +} + +/* Check if we're at the end of line, w/ comments */ +static bool is_eol(git_parse_ctx *ctx) +{ + char c; + return git_parse_peek(&c, ctx, GIT_PARSE_PEEK_SKIP_WHITESPACE) < 0 || c == '#'; +} + +static int advance_until( + const char **start, size_t *len, git_parse_ctx *ctx, char needle) +{ + *start = ctx->line; + while (ctx->line_len > 0 && *ctx->line != '#' && *ctx->line != needle) + git_parse_advance_chars(ctx, 1); + + if (ctx->line_len == 0 || *ctx->line == '#') + return -1; /* end of line */ + + *len = ctx->line - *start; + git_parse_advance_chars(ctx, 1); /* advance past needle */ + return 0; +} + +/* + * Parse a single entry from a mailmap file. + * + * The output git_strs will be non-owning, and should be copied before being + * persisted. + */ +static int parse_mailmap_entry( + git_str *real_name, git_str *real_email, + git_str *replace_name, git_str *replace_email, + git_parse_ctx *ctx) +{ + const char *start; + size_t len; + + git_str_clear(real_name); + git_str_clear(real_email); + git_str_clear(replace_name); + git_str_clear(replace_email); + + git_parse_advance_ws(ctx); + if (is_eol(ctx)) + return -1; /* blank line */ + + /* Parse the real name */ + if (advance_until(&start, &len, ctx, '<') < 0) + return -1; + + git_str_attach_notowned(real_name, start, len); + git_str_rtrim(real_name); + + /* + * If this is the last email in the line, this is the email to replace, + * otherwise, it's the real email. + */ + if (advance_until(&start, &len, ctx, '>') < 0) + return -1; + + /* If we aren't at the end of the line, parse a second name and email */ + if (!is_eol(ctx)) { + git_str_attach_notowned(real_email, start, len); + + git_parse_advance_ws(ctx); + if (advance_until(&start, &len, ctx, '<') < 0) + return -1; + git_str_attach_notowned(replace_name, start, len); + git_str_rtrim(replace_name); + + if (advance_until(&start, &len, ctx, '>') < 0) + return -1; + } + + git_str_attach_notowned(replace_email, start, len); + + if (!is_eol(ctx)) + return -1; + + return 0; +} + +int git_mailmap_new(git_mailmap **out) +{ + int error; + git_mailmap *mm = git__calloc(1, sizeof(git_mailmap)); + GIT_ERROR_CHECK_ALLOC(mm); + + error = git_vector_init(&mm->entries, 0, mailmap_entry_cmp); + if (error < 0) { + git__free(mm); + return error; + } + *out = mm; + return 0; +} + +void git_mailmap_free(git_mailmap *mm) +{ + size_t idx; + git_mailmap_entry *entry; + if (!mm) + return; + + git_vector_foreach(&mm->entries, idx, entry) + mailmap_entry_free(entry); + + git_vector_free(&mm->entries); + git__free(mm); +} + +static int mailmap_add_entry_unterminated( + git_mailmap *mm, + const char *real_name, size_t real_name_size, + const char *real_email, size_t real_email_size, + const char *replace_name, size_t replace_name_size, + const char *replace_email, size_t replace_email_size) +{ + int error; + git_mailmap_entry *entry = git__calloc(1, sizeof(git_mailmap_entry)); + GIT_ERROR_CHECK_ALLOC(entry); + + GIT_ASSERT_ARG(mm); + GIT_ASSERT_ARG(replace_email && *replace_email); + + if (real_name_size > 0) { + entry->real_name = git__substrdup(real_name, real_name_size); + GIT_ERROR_CHECK_ALLOC(entry->real_name); + } + if (real_email_size > 0) { + entry->real_email = git__substrdup(real_email, real_email_size); + GIT_ERROR_CHECK_ALLOC(entry->real_email); + } + if (replace_name_size > 0) { + entry->replace_name = git__substrdup(replace_name, replace_name_size); + GIT_ERROR_CHECK_ALLOC(entry->replace_name); + } + entry->replace_email = git__substrdup(replace_email, replace_email_size); + GIT_ERROR_CHECK_ALLOC(entry->replace_email); + + error = git_vector_insert_sorted(&mm->entries, entry, mailmap_entry_replace); + if (error == GIT_EEXISTS) + error = GIT_OK; + else if (error < 0) + mailmap_entry_free(entry); + + return error; +} + +int git_mailmap_add_entry( + git_mailmap *mm, const char *real_name, const char *real_email, + const char *replace_name, const char *replace_email) +{ + return mailmap_add_entry_unterminated( + mm, + real_name, real_name ? strlen(real_name) : 0, + real_email, real_email ? strlen(real_email) : 0, + replace_name, replace_name ? strlen(replace_name) : 0, + replace_email, strlen(replace_email)); +} + +static int mailmap_add_buffer(git_mailmap *mm, const char *buf, size_t len) +{ + int error = 0; + git_parse_ctx ctx; + + /* Scratch buffers containing the real parsed names & emails */ + git_str real_name = GIT_STR_INIT; + git_str real_email = GIT_STR_INIT; + git_str replace_name = GIT_STR_INIT; + git_str replace_email = GIT_STR_INIT; + + /* Buffers may not contain '\0's. */ + if (memchr(buf, '\0', len) != NULL) + return -1; + + git_parse_ctx_init(&ctx, buf, len); + + /* Run the parser */ + while (ctx.remain_len > 0) { + error = parse_mailmap_entry( + &real_name, &real_email, &replace_name, &replace_email, &ctx); + if (error < 0) { + error = 0; /* Skip lines which don't contain a valid entry */ + git_parse_advance_line(&ctx); + continue; /* TODO: warn */ + } + + /* NOTE: Can't use add_entry(...) as our buffers aren't terminated */ + error = mailmap_add_entry_unterminated( + mm, real_name.ptr, real_name.size, real_email.ptr, real_email.size, + replace_name.ptr, replace_name.size, replace_email.ptr, replace_email.size); + if (error < 0) + goto cleanup; + + error = 0; + } + +cleanup: + git_str_dispose(&real_name); + git_str_dispose(&real_email); + git_str_dispose(&replace_name); + git_str_dispose(&replace_email); + return error; +} + +int git_mailmap_from_buffer(git_mailmap **out, const char *data, size_t len) +{ + int error = git_mailmap_new(out); + if (error < 0) + return error; + + error = mailmap_add_buffer(*out, data, len); + if (error < 0) { + git_mailmap_free(*out); + *out = NULL; + } + return error; +} + +static int mailmap_add_blob( + git_mailmap *mm, git_repository *repo, const char *rev) +{ + git_object *object = NULL; + git_blob *blob = NULL; + git_str content = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(mm); + GIT_ASSERT_ARG(repo); + + error = git_revparse_single(&object, repo, rev); + if (error < 0) + goto cleanup; + + error = git_object_peel((git_object **)&blob, object, GIT_OBJECT_BLOB); + if (error < 0) + goto cleanup; + + error = git_blob__getbuf(&content, blob); + if (error < 0) + goto cleanup; + + error = mailmap_add_buffer(mm, content.ptr, content.size); + if (error < 0) + goto cleanup; + +cleanup: + git_str_dispose(&content); + git_blob_free(blob); + git_object_free(object); + return error; +} + +static int mailmap_add_file_ondisk( + git_mailmap *mm, const char *path, git_repository *repo) +{ + const char *base = repo ? git_repository_workdir(repo) : NULL; + git_str fullpath = GIT_STR_INIT; + git_str content = GIT_STR_INIT; + int error; + + error = git_fs_path_join_unrooted(&fullpath, path, base, NULL); + if (error < 0) + goto cleanup; + + error = git_path_validate_str_length(repo, &fullpath); + if (error < 0) + goto cleanup; + + error = git_futils_readbuffer(&content, fullpath.ptr); + if (error < 0) + goto cleanup; + + error = mailmap_add_buffer(mm, content.ptr, content.size); + if (error < 0) + goto cleanup; + +cleanup: + git_str_dispose(&fullpath); + git_str_dispose(&content); + return error; +} + +/* NOTE: Only expose with an error return, currently never errors */ +static void mailmap_add_from_repository(git_mailmap *mm, git_repository *repo) +{ + git_config *config = NULL; + git_str rev_buf = GIT_STR_INIT; + git_str path_buf = GIT_STR_INIT; + const char *rev = NULL; + const char *path = NULL; + + /* If we're in a bare repo, default blob to 'HEAD:.mailmap' */ + if (repo->is_bare) + rev = MM_BLOB_DEFAULT; + + /* Try to load 'mailmap.file' and 'mailmap.blob' cfgs from the repo */ + if (git_repository_config(&config, repo) == 0) { + if (git_config__get_string_buf(&rev_buf, config, MM_BLOB_CONFIG) == 0) + rev = rev_buf.ptr; + if (git_config__get_path(&path_buf, config, MM_FILE_CONFIG) == 0) + path = path_buf.ptr; + } + + /* + * Load mailmap files in order, overriding previous entries with new ones. + * 1. The '.mailmap' file in the repository's workdir root, + * 2. The blob described by the 'mailmap.blob' config (default HEAD:.mailmap), + * 3. The file described by the 'mailmap.file' config. + * + * We ignore errors from these loads, as these files may not exist, or may + * contain invalid information, and we don't want to report that error. + * + * XXX: Warn? + */ + if (!repo->is_bare) + mailmap_add_file_ondisk(mm, MM_FILE, repo); + if (rev != NULL) + mailmap_add_blob(mm, repo, rev); + if (path != NULL) + mailmap_add_file_ondisk(mm, path, repo); + + git_str_dispose(&rev_buf); + git_str_dispose(&path_buf); + git_config_free(config); +} + +int git_mailmap_from_repository(git_mailmap **out, git_repository *repo) +{ + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + if ((error = git_mailmap_new(out)) < 0) + return error; + + mailmap_add_from_repository(*out, repo); + return 0; +} + +const git_mailmap_entry *git_mailmap_entry_lookup( + const git_mailmap *mm, const char *name, const char *email) +{ + int error; + ssize_t fallback = -1; + size_t idx; + git_mailmap_entry *entry; + + /* The lookup needle we want to use only sets the replace_email. */ + git_mailmap_entry needle = { NULL }; + needle.replace_email = (char *)email; + + GIT_ASSERT_ARG_WITH_RETVAL(email, NULL); + + if (!mm) + return NULL; + + /* + * We want to find the place to start looking. so we do a binary search for + * the "fallback" nameless entry. If we find it, we advance past it and record + * the index. + */ + error = git_vector_bsearch(&idx, (git_vector *)&mm->entries, &needle); + if (error >= 0) + fallback = idx++; + else if (error != GIT_ENOTFOUND) + return NULL; + + /* do a linear search for an exact match */ + for (; idx < git_vector_length(&mm->entries); ++idx) { + entry = git_vector_get(&mm->entries, idx); + + if (git__strcmp(entry->replace_email, email)) + break; /* it's a different email, so we're done looking */ + + /* should be specific */ + GIT_ASSERT_WITH_RETVAL(entry->replace_name, NULL); + if (!name || !git__strcmp(entry->replace_name, name)) + return entry; + } + + if (fallback < 0) + return NULL; /* no fallback */ + return git_vector_get(&mm->entries, fallback); +} + +int git_mailmap_resolve( + const char **real_name, const char **real_email, + const git_mailmap *mailmap, + const char *name, const char *email) +{ + const git_mailmap_entry *entry = NULL; + + GIT_ASSERT(name); + GIT_ASSERT(email); + + *real_name = name; + *real_email = email; + + if ((entry = git_mailmap_entry_lookup(mailmap, name, email))) { + if (entry->real_name) + *real_name = entry->real_name; + if (entry->real_email) + *real_email = entry->real_email; + } + return 0; +} + +int git_mailmap_resolve_signature( + git_signature **out, const git_mailmap *mailmap, const git_signature *sig) +{ + const char *name = NULL; + const char *email = NULL; + int error; + + if (!sig) + return 0; + + error = git_mailmap_resolve(&name, &email, mailmap, sig->name, sig->email); + if (error < 0) + return error; + + error = git_signature_new(out, name, email, sig->when.time, sig->when.offset); + if (error < 0) + return error; + + /* Copy over the sign, as git_signature_new doesn't let you pass it. */ + (*out)->when.sign = sig->when.sign; + return 0; +} diff --git a/src/libgit2/mailmap.h b/src/libgit2/mailmap.h new file mode 100644 index 0000000..2c9736a --- /dev/null +++ b/src/libgit2/mailmap.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_mailmap_h__ +#define INCLUDE_mailmap_h__ + +#include "git2/mailmap.h" +#include "vector.h" + +/* + * A mailmap is stored as a sorted vector of 'git_mailmap_entry's. These entries + * are sorted first by 'replace_email', and then by 'replace_name'. NULL + * replace_names are ordered first. + * + * Looking up a name and email in the mailmap is done with a binary search. + */ +struct git_mailmap { + git_vector entries; +}; + +/* Single entry parsed from a mailmap */ +typedef struct git_mailmap_entry { + char *real_name; /**< the real name (may be NULL) */ + char *real_email; /**< the real email (may be NULL) */ + char *replace_name; /**< the name to replace (may be NULL) */ + char *replace_email; /**< the email to replace */ +} git_mailmap_entry; + +const git_mailmap_entry *git_mailmap_entry_lookup( + const git_mailmap *mm, const char *name, const char *email); + +#endif diff --git a/src/libgit2/merge.c b/src/libgit2/merge.c new file mode 100644 index 0000000..0114e4b --- /dev/null +++ b/src/libgit2/merge.c @@ -0,0 +1,3440 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "merge.h" + +#include "posix.h" +#include "str.h" +#include "repository.h" +#include "revwalk.h" +#include "commit_list.h" +#include "fs_path.h" +#include "refs.h" +#include "object.h" +#include "iterator.h" +#include "refs.h" +#include "diff.h" +#include "diff_generate.h" +#include "diff_tform.h" +#include "checkout.h" +#include "tree.h" +#include "blob.h" +#include "oid.h" +#include "index.h" +#include "filebuf.h" +#include "config.h" +#include "oidarray.h" +#include "annotated_commit.h" +#include "commit.h" +#include "oidarray.h" +#include "merge_driver.h" +#include "oidmap.h" +#include "array.h" + +#include "git2/types.h" +#include "git2/repository.h" +#include "git2/object.h" +#include "git2/commit.h" +#include "git2/merge.h" +#include "git2/refs.h" +#include "git2/reset.h" +#include "git2/checkout.h" +#include "git2/signature.h" +#include "git2/config.h" +#include "git2/tree.h" +#include "git2/oidarray.h" +#include "git2/annotated_commit.h" +#include "git2/sys/index.h" +#include "git2/sys/hashsig.h" + +#define GIT_MERGE_INDEX_ENTRY_EXISTS(X) ((X).mode != 0) +#define GIT_MERGE_INDEX_ENTRY_ISFILE(X) S_ISREG((X).mode) + + +typedef enum { + TREE_IDX_ANCESTOR = 0, + TREE_IDX_OURS = 1, + TREE_IDX_THEIRS = 2 +} merge_tree_index_t; + +/* Tracks D/F conflicts */ +struct merge_diff_df_data { + const char *df_path; + const char *prev_path; + git_merge_diff *prev_conflict; +}; + +/* + * This acts as a negative cache entry marker. In case we've tried to calculate + * similarity metrics for a given blob already but `git_hashsig` determined + * that it's too small in order to have a meaningful hash signature, we will + * insert the address of this marker instead of `NULL`. Like this, we can + * easily check whether we have checked a gien entry already and skip doing the + * calculation again and again. + */ +static int cache_invalid_marker; + +/* Merge base computation */ + +static int merge_bases_many(git_commit_list **out, git_revwalk **walk_out, git_repository *repo, size_t length, const git_oid input_array[]) +{ + git_revwalk *walk = NULL; + git_vector list; + git_commit_list *result = NULL; + git_commit_list_node *commit; + int error = -1; + unsigned int i; + + if (length < 2) { + git_error_set(GIT_ERROR_INVALID, "at least two commits are required to find an ancestor"); + return -1; + } + + if (git_vector_init(&list, length - 1, NULL) < 0) + return -1; + + if (git_revwalk_new(&walk, repo) < 0) + goto on_error; + + for (i = 1; i < length; i++) { + commit = git_revwalk__commit_lookup(walk, &input_array[i]); + if (commit == NULL) + goto on_error; + + git_vector_insert(&list, commit); + } + + commit = git_revwalk__commit_lookup(walk, &input_array[0]); + if (commit == NULL) + goto on_error; + + if (git_merge__bases_many(&result, walk, commit, &list, 0) < 0) + goto on_error; + + if (!result) { + git_error_set(GIT_ERROR_MERGE, "no merge base found"); + error = GIT_ENOTFOUND; + goto on_error; + } + + *out = result; + *walk_out = walk; + + git_vector_free(&list); + return 0; + +on_error: + git_vector_free(&list); + git_revwalk_free(walk); + return error; +} + +int git_merge_base_many(git_oid *out, git_repository *repo, size_t length, const git_oid input_array[]) +{ + git_revwalk *walk; + git_commit_list *result = NULL; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(input_array); + + if ((error = merge_bases_many(&result, &walk, repo, length, input_array)) < 0) + return error; + + git_oid_cpy(out, &result->item->oid); + + git_commit_list_free(&result); + git_revwalk_free(walk); + + return 0; +} + +int git_merge_bases_many(git_oidarray *out, git_repository *repo, size_t length, const git_oid input_array[]) +{ + git_revwalk *walk; + git_commit_list *list, *result = NULL; + int error = 0; + git_array_oid_t array; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(input_array); + + if ((error = merge_bases_many(&result, &walk, repo, length, input_array)) < 0) + return error; + + git_array_init(array); + + list = result; + while (list) { + git_oid *id = git_array_alloc(array); + if (id == NULL) { + error = -1; + goto cleanup; + } + + git_oid_cpy(id, &list->item->oid); + list = list->next; + } + + git_oidarray__from_array(out, &array); + +cleanup: + git_commit_list_free(&result); + git_revwalk_free(walk); + + return error; +} + +int git_merge_base_octopus(git_oid *out, git_repository *repo, size_t length, const git_oid input_array[]) +{ + git_oid result; + unsigned int i; + int error = -1; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(input_array); + + if (length < 2) { + git_error_set(GIT_ERROR_INVALID, "at least two commits are required to find an ancestor"); + return -1; + } + + result = input_array[0]; + for (i = 1; i < length; i++) { + error = git_merge_base(&result, repo, &result, &input_array[i]); + if (error < 0) + return error; + } + + *out = result; + + return 0; +} + +static int merge_bases(git_commit_list **out, git_revwalk **walk_out, git_repository *repo, const git_oid *one, const git_oid *two) +{ + git_revwalk *walk; + git_vector list; + git_commit_list *result = NULL; + git_commit_list_node *commit; + void *contents[1]; + + if (git_revwalk_new(&walk, repo) < 0) + return -1; + + commit = git_revwalk__commit_lookup(walk, two); + if (commit == NULL) + goto on_error; + + /* This is just one value, so we can do it on the stack */ + memset(&list, 0x0, sizeof(git_vector)); + contents[0] = commit; + list.length = 1; + list.contents = contents; + + commit = git_revwalk__commit_lookup(walk, one); + if (commit == NULL) + goto on_error; + + if (git_merge__bases_many(&result, walk, commit, &list, 0) < 0) + goto on_error; + + if (!result) { + git_revwalk_free(walk); + git_error_set(GIT_ERROR_MERGE, "no merge base found"); + return GIT_ENOTFOUND; + } + + *out = result; + *walk_out = walk; + + return 0; + +on_error: + git_revwalk_free(walk); + return -1; + +} + +int git_merge_base(git_oid *out, git_repository *repo, const git_oid *one, const git_oid *two) +{ + int error; + git_revwalk *walk; + git_commit_list *result; + + if ((error = merge_bases(&result, &walk, repo, one, two)) < 0) + return error; + + git_oid_cpy(out, &result->item->oid); + git_commit_list_free(&result); + git_revwalk_free(walk); + + return 0; +} + +int git_merge_bases(git_oidarray *out, git_repository *repo, const git_oid *one, const git_oid *two) +{ + int error; + git_revwalk *walk; + git_commit_list *result, *list; + git_array_oid_t array; + + git_array_init(array); + + if ((error = merge_bases(&result, &walk, repo, one, two)) < 0) + return error; + + list = result; + while (list) { + git_oid *id = git_array_alloc(array); + if (id == NULL) + goto on_error; + + git_oid_cpy(id, &list->item->oid); + list = list->next; + } + + git_oidarray__from_array(out, &array); + git_commit_list_free(&result); + git_revwalk_free(walk); + + return 0; + +on_error: + git_commit_list_free(&result); + git_revwalk_free(walk); + return -1; +} + +static int interesting(git_pqueue *list) +{ + size_t i; + + for (i = 0; i < git_pqueue_size(list); i++) { + git_commit_list_node *commit = git_pqueue_get(list, i); + if ((commit->flags & STALE) == 0) + return 1; + } + + return 0; +} + +static int clear_commit_marks_1(git_commit_list **plist, + git_commit_list_node *commit, unsigned int mark) +{ + while (commit) { + unsigned int i; + + if (!(mark & commit->flags)) + return 0; + + commit->flags &= ~mark; + + for (i = 1; i < commit->out_degree; i++) { + git_commit_list_node *p = commit->parents[i]; + if (git_commit_list_insert(p, plist) == NULL) + return -1; + } + + commit = commit->out_degree ? commit->parents[0] : NULL; + } + + return 0; +} + +static int clear_commit_marks_many(git_vector *commits, unsigned int mark) +{ + git_commit_list *list = NULL; + git_commit_list_node *c; + unsigned int i; + + git_vector_foreach(commits, i, c) { + if (git_commit_list_insert(c, &list) == NULL) + return -1; + } + + while (list) + if (clear_commit_marks_1(&list, git_commit_list_pop(&list), mark) < 0) + return -1; + return 0; +} + +static int clear_commit_marks(git_commit_list_node *commit, unsigned int mark) +{ + git_commit_list *list = NULL; + if (git_commit_list_insert(commit, &list) == NULL) + return -1; + while (list) + if (clear_commit_marks_1(&list, git_commit_list_pop(&list), mark) < 0) + return -1; + return 0; +} + +static int paint_down_to_common( + git_commit_list **out, + git_revwalk *walk, + git_commit_list_node *one, + git_vector *twos, + uint32_t minimum_generation) +{ + git_pqueue list; + git_commit_list *result = NULL; + git_commit_list_node *two; + + int error; + unsigned int i; + + if (git_pqueue_init(&list, 0, twos->length * 2, git_commit_list_generation_cmp) < 0) + return -1; + + one->flags |= PARENT1; + if (git_pqueue_insert(&list, one) < 0) + return -1; + + git_vector_foreach(twos, i, two) { + if (git_commit_list_parse(walk, two) < 0) + return -1; + + two->flags |= PARENT2; + if (git_pqueue_insert(&list, two) < 0) + return -1; + } + + /* as long as there are non-STALE commits */ + while (interesting(&list)) { + git_commit_list_node *commit = git_pqueue_pop(&list); + int flags; + + if (commit == NULL) + break; + + flags = commit->flags & (PARENT1 | PARENT2 | STALE); + if (flags == (PARENT1 | PARENT2)) { + if (!(commit->flags & RESULT)) { + commit->flags |= RESULT; + if (git_commit_list_insert(commit, &result) == NULL) + return -1; + } + /* we mark the parents of a merge stale */ + flags |= STALE; + } + + for (i = 0; i < commit->out_degree; i++) { + git_commit_list_node *p = commit->parents[i]; + if ((p->flags & flags) == flags) + continue; + if (p->generation < minimum_generation) + continue; + + if ((error = git_commit_list_parse(walk, p)) < 0) + return error; + + p->flags |= flags; + if (git_pqueue_insert(&list, p) < 0) + return -1; + } + } + + git_pqueue_free(&list); + *out = result; + return 0; +} + +static int remove_redundant(git_revwalk *walk, git_vector *commits, uint32_t minimum_generation) +{ + git_vector work = GIT_VECTOR_INIT; + unsigned char *redundant; + unsigned int *filled_index; + unsigned int i, j; + int error = 0; + + redundant = git__calloc(commits->length, 1); + GIT_ERROR_CHECK_ALLOC(redundant); + filled_index = git__calloc((commits->length - 1), sizeof(unsigned int)); + GIT_ERROR_CHECK_ALLOC(filled_index); + + for (i = 0; i < commits->length; ++i) { + if ((error = git_commit_list_parse(walk, commits->contents[i])) < 0) + goto done; + } + + for (i = 0; i < commits->length; ++i) { + git_commit_list *common = NULL; + git_commit_list_node *commit = commits->contents[i]; + + if (redundant[i]) + continue; + + git_vector_clear(&work); + + for (j = 0; j < commits->length; j++) { + if (i == j || redundant[j]) + continue; + + filled_index[work.length] = j; + if ((error = git_vector_insert(&work, commits->contents[j])) < 0) + goto done; + } + + error = paint_down_to_common(&common, walk, commit, &work, minimum_generation); + if (error < 0) + goto done; + + if (commit->flags & PARENT2) + redundant[i] = 1; + + for (j = 0; j < work.length; j++) { + git_commit_list_node *w = work.contents[j]; + if (w->flags & PARENT1) + redundant[filled_index[j]] = 1; + } + + git_commit_list_free(&common); + + if ((error = clear_commit_marks(commit, ALL_FLAGS)) < 0 || + (error = clear_commit_marks_many(&work, ALL_FLAGS)) < 0) + goto done; + } + + for (i = 0; i < commits->length; ++i) { + if (redundant[i]) + commits->contents[i] = NULL; + } + +done: + git__free(redundant); + git__free(filled_index); + git_vector_free(&work); + return error; +} + +int git_merge__bases_many( + git_commit_list **out, + git_revwalk *walk, + git_commit_list_node *one, + git_vector *twos, + uint32_t minimum_generation) +{ + int error; + unsigned int i; + git_commit_list_node *two; + git_commit_list *result = NULL, *tmp = NULL; + + /* If there's only the one commit, there can be no merge bases */ + if (twos->length == 0) { + *out = NULL; + return 0; + } + + /* if the commit is repeated, we have a our merge base already */ + git_vector_foreach(twos, i, two) { + if (one == two) + return git_commit_list_insert(one, out) ? 0 : -1; + } + + if (git_commit_list_parse(walk, one) < 0) + return -1; + + error = paint_down_to_common(&result, walk, one, twos, minimum_generation); + if (error < 0) + return error; + + /* filter out any stale commits in the results */ + tmp = result; + result = NULL; + + while (tmp) { + git_commit_list_node *c = git_commit_list_pop(&tmp); + if (!(c->flags & STALE)) + if (git_commit_list_insert_by_date(c, &result) == NULL) + return -1; + } + + /* + * more than one merge base -- see if there are redundant merge + * bases and remove them + */ + if (result && result->next) { + git_vector redundant = GIT_VECTOR_INIT; + + while (result) + git_vector_insert(&redundant, git_commit_list_pop(&result)); + + if ((error = clear_commit_marks(one, ALL_FLAGS)) < 0 || + (error = clear_commit_marks_many(twos, ALL_FLAGS)) < 0 || + (error = remove_redundant(walk, &redundant, minimum_generation)) < 0) { + git_vector_free(&redundant); + return error; + } + + git_vector_foreach(&redundant, i, two) { + if (two != NULL) + git_commit_list_insert_by_date(two, &result); + } + + git_vector_free(&redundant); + } + + *out = result; + return 0; +} + +int git_repository_mergehead_foreach( + git_repository *repo, + git_repository_mergehead_foreach_cb cb, + void *payload) +{ + git_str merge_head_path = GIT_STR_INIT, merge_head_file = GIT_STR_INIT; + char *buffer, *line; + size_t line_num = 1; + git_oid oid; + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(cb); + + if ((error = git_str_joinpath(&merge_head_path, repo->gitdir, + GIT_MERGE_HEAD_FILE)) < 0) + return error; + + if ((error = git_futils_readbuffer(&merge_head_file, + git_str_cstr(&merge_head_path))) < 0) + goto cleanup; + + buffer = merge_head_file.ptr; + + while ((line = git__strsep(&buffer, "\n")) != NULL) { + if (strlen(line) != git_oid_hexsize(repo->oid_type)) { + git_error_set(GIT_ERROR_INVALID, "unable to parse OID - invalid length"); + error = -1; + goto cleanup; + } + + if ((error = git_oid__fromstr(&oid, line, repo->oid_type)) < 0) + goto cleanup; + + if ((error = cb(&oid, payload)) != 0) { + git_error_set_after_callback(error); + goto cleanup; + } + + ++line_num; + } + + if (*buffer) { + git_error_set(GIT_ERROR_MERGE, "no EOL at line %"PRIuZ, line_num); + error = -1; + goto cleanup; + } + +cleanup: + git_str_dispose(&merge_head_path); + git_str_dispose(&merge_head_file); + + return error; +} + +GIT_INLINE(int) index_entry_cmp(const git_index_entry *a, const git_index_entry *b) +{ + int value = 0; + + if (a->path == NULL) + return (b->path == NULL) ? 0 : 1; + + if ((value = a->mode - b->mode) == 0 && + (value = git_oid__cmp(&a->id, &b->id)) == 0) + value = strcmp(a->path, b->path); + + return value; +} + +/* Conflict resolution */ + +static int merge_conflict_resolve_trivial( + int *resolved, + git_merge_diff_list *diff_list, + const git_merge_diff *conflict) +{ + int ours_empty, theirs_empty; + int ours_changed, theirs_changed, ours_theirs_differ; + git_index_entry const *result = NULL; + int error = 0; + + GIT_ASSERT_ARG(resolved); + GIT_ASSERT_ARG(diff_list); + GIT_ASSERT_ARG(conflict); + + *resolved = 0; + + if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE || + conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED) + return 0; + + if (conflict->our_status == GIT_DELTA_RENAMED || + conflict->their_status == GIT_DELTA_RENAMED) + return 0; + + ours_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry); + theirs_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry); + + ours_changed = (conflict->our_status != GIT_DELTA_UNMODIFIED); + theirs_changed = (conflict->their_status != GIT_DELTA_UNMODIFIED); + ours_theirs_differ = ours_changed && theirs_changed && + index_entry_cmp(&conflict->our_entry, &conflict->their_entry); + + /* + * Note: with only one ancestor, some cases are not distinct: + * + * 16: ancest:anc1/anc2, head:anc1, remote:anc2 = result:no merge + * 3: ancest:(empty)^, head:head, remote:(empty) = result:no merge + * 2: ancest:(empty)^, head:(empty), remote:remote = result:no merge + * + * Note that the two cases that take D/F conflicts into account + * specifically do not need to be explicitly tested, as D/F conflicts + * would fail the *empty* test: + * + * 3ALT: ancest:(empty)+, head:head, remote:*empty* = result:head + * 2ALT: ancest:(empty)+, head:*empty*, remote:remote = result:remote + * + * Note that many of these cases need not be explicitly tested, as + * they simply degrade to "all different" cases (eg, 11): + * + * 4: ancest:(empty)^, head:head, remote:remote = result:no merge + * 7: ancest:ancest+, head:(empty), remote:remote = result:no merge + * 9: ancest:ancest+, head:head, remote:(empty) = result:no merge + * 11: ancest:ancest+, head:head, remote:remote = result:no merge + */ + + /* 5ALT: ancest:*, head:head, remote:head = result:head */ + if (ours_changed && !ours_empty && !ours_theirs_differ) + result = &conflict->our_entry; + /* 6: ancest:ancest+, head:(empty), remote:(empty) = result:no merge */ + else if (ours_changed && ours_empty && theirs_empty) + *resolved = 0; + /* 8: ancest:ancest^, head:(empty), remote:ancest = result:no merge */ + else if (ours_empty && !theirs_changed) + *resolved = 0; + /* 10: ancest:ancest^, head:ancest, remote:(empty) = result:no merge */ + else if (!ours_changed && theirs_empty) + *resolved = 0; + /* 13: ancest:ancest+, head:head, remote:ancest = result:head */ + else if (ours_changed && !theirs_changed) + result = &conflict->our_entry; + /* 14: ancest:ancest+, head:ancest, remote:remote = result:remote */ + else if (!ours_changed && theirs_changed) + result = &conflict->their_entry; + else + *resolved = 0; + + if (result != NULL && + GIT_MERGE_INDEX_ENTRY_EXISTS(*result) && + (error = git_vector_insert(&diff_list->staged, (void *)result)) >= 0) + *resolved = 1; + + /* Note: trivial resolution does not update the REUC. */ + + return error; +} + +static int merge_conflict_resolve_one_removed( + int *resolved, + git_merge_diff_list *diff_list, + const git_merge_diff *conflict) +{ + int ours_empty, theirs_empty; + int ours_changed, theirs_changed; + int error = 0; + + GIT_ASSERT_ARG(resolved); + GIT_ASSERT_ARG(diff_list); + GIT_ASSERT_ARG(conflict); + + *resolved = 0; + + if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE || + conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED) + return 0; + + ours_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry); + theirs_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry); + + ours_changed = (conflict->our_status != GIT_DELTA_UNMODIFIED); + theirs_changed = (conflict->their_status != GIT_DELTA_UNMODIFIED); + + /* Removed in both */ + if (ours_changed && ours_empty && theirs_empty) + *resolved = 1; + /* Removed in ours */ + else if (ours_empty && !theirs_changed) + *resolved = 1; + /* Removed in theirs */ + else if (!ours_changed && theirs_empty) + *resolved = 1; + + if (*resolved) + git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict); + + return error; +} + +static int merge_conflict_resolve_one_renamed( + int *resolved, + git_merge_diff_list *diff_list, + const git_merge_diff *conflict) +{ + int ours_renamed, theirs_renamed; + int ours_changed, theirs_changed; + git_index_entry *merged; + int error = 0; + + GIT_ASSERT_ARG(resolved); + GIT_ASSERT_ARG(diff_list); + GIT_ASSERT_ARG(conflict); + + *resolved = 0; + + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) || + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry)) + return 0; + + ours_renamed = (conflict->our_status == GIT_DELTA_RENAMED); + theirs_renamed = (conflict->their_status == GIT_DELTA_RENAMED); + + if (!ours_renamed && !theirs_renamed) + return 0; + + /* Reject one file in a 2->1 conflict */ + if (conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 || + conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_1_TO_2 || + conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED) + return 0; + + ours_changed = (git_oid__cmp(&conflict->ancestor_entry.id, &conflict->our_entry.id) != 0) || + (conflict->ancestor_entry.mode != conflict->our_entry.mode); + + theirs_changed = (git_oid__cmp(&conflict->ancestor_entry.id, &conflict->their_entry.id) != 0) || + (conflict->ancestor_entry.mode != conflict->their_entry.mode); + + /* if both are modified (and not to a common target) require a merge */ + if (ours_changed && theirs_changed && + git_oid__cmp(&conflict->our_entry.id, &conflict->their_entry.id) != 0) + return 0; + + if ((merged = git_pool_malloc(&diff_list->pool, sizeof(git_index_entry))) == NULL) + return -1; + + if (ours_changed) + memcpy(merged, &conflict->our_entry, sizeof(git_index_entry)); + else + memcpy(merged, &conflict->their_entry, sizeof(git_index_entry)); + + if (ours_renamed) + merged->path = conflict->our_entry.path; + else + merged->path = conflict->their_entry.path; + + *resolved = 1; + + git_vector_insert(&diff_list->staged, merged); + git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict); + + return error; +} + +static bool merge_conflict_can_resolve_contents( + const git_merge_diff *conflict) +{ + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) || + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry)) + return false; + + /* Reject D/F conflicts */ + if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE) + return false; + + /* Reject submodules. */ + if (S_ISGITLINK(conflict->ancestor_entry.mode) || + S_ISGITLINK(conflict->our_entry.mode) || + S_ISGITLINK(conflict->their_entry.mode)) + return false; + + /* Reject link/file conflicts. */ + if ((S_ISLNK(conflict->ancestor_entry.mode) ^ + S_ISLNK(conflict->our_entry.mode)) || + (S_ISLNK(conflict->ancestor_entry.mode) ^ + S_ISLNK(conflict->their_entry.mode))) + return false; + + /* Reject name conflicts */ + if (conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 || + conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED) + return false; + + if ((conflict->our_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED && + (conflict->their_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED && + strcmp(conflict->ancestor_entry.path, conflict->their_entry.path) != 0) + return false; + + return true; +} + +static int merge_conflict_invoke_driver( + git_index_entry **out, + const char *name, + git_merge_driver *driver, + git_merge_diff_list *diff_list, + git_merge_driver_source *src) +{ + git_index_entry *result; + git_buf buf = {0}; + const char *path; + uint32_t mode; + git_odb *odb = NULL; + git_oid oid; + int error; + + *out = NULL; + + if ((error = driver->apply(driver, &path, &mode, &buf, name, src)) < 0 || + (error = git_repository_odb(&odb, src->repo)) < 0 || + (error = git_odb_write(&oid, odb, buf.ptr, buf.size, GIT_OBJECT_BLOB)) < 0) + goto done; + + result = git_pool_mallocz(&diff_list->pool, sizeof(git_index_entry)); + GIT_ERROR_CHECK_ALLOC(result); + + git_oid_cpy(&result->id, &oid); + result->mode = mode; + result->file_size = (uint32_t)buf.size; + + result->path = git_pool_strdup(&diff_list->pool, path); + GIT_ERROR_CHECK_ALLOC(result->path); + + *out = result; + +done: + git_buf_dispose(&buf); + git_odb_free(odb); + + return error; +} + +static int merge_conflict_resolve_contents( + int *resolved, + git_merge_diff_list *diff_list, + const git_merge_diff *conflict, + const git_merge_options *merge_opts, + const git_merge_file_options *file_opts) +{ + git_merge_driver_source source = {0}; + git_merge_file_result result = {0}; + git_merge_driver *driver; + git_merge_driver__builtin builtin = {{0}}; + git_index_entry *merge_result; + git_odb *odb = NULL; + const char *name; + bool fallback = false; + int error; + + GIT_ASSERT_ARG(resolved); + GIT_ASSERT_ARG(diff_list); + GIT_ASSERT_ARG(conflict); + + *resolved = 0; + + if (!merge_conflict_can_resolve_contents(conflict)) + return 0; + + source.repo = diff_list->repo; + source.default_driver = merge_opts->default_driver; + source.file_opts = file_opts; + source.ancestor = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ? + &conflict->ancestor_entry : NULL; + source.ours = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ? + &conflict->our_entry : NULL; + source.theirs = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ? + &conflict->their_entry : NULL; + + if (file_opts->favor != GIT_MERGE_FILE_FAVOR_NORMAL) { + /* if the user requested a particular type of resolution (via the + * favor flag) then let that override the gitattributes and use + * the builtin driver. + */ + name = "text"; + builtin.base.apply = git_merge_driver__builtin_apply; + builtin.favor = file_opts->favor; + + driver = &builtin.base; + } else { + /* find the merge driver for this file */ + if ((error = git_merge_driver_for_source(&name, &driver, &source)) < 0) + goto done; + + if (driver == NULL) + fallback = true; + } + + if (driver) { + error = merge_conflict_invoke_driver(&merge_result, name, driver, + diff_list, &source); + + if (error == GIT_PASSTHROUGH) + fallback = true; + } + + if (fallback) { + error = merge_conflict_invoke_driver(&merge_result, "text", + &git_merge_driver__text.base, diff_list, &source); + } + + if (error < 0) { + if (error == GIT_EMERGECONFLICT) + error = 0; + + goto done; + } + + git_vector_insert(&diff_list->staged, merge_result); + git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict); + + *resolved = 1; + +done: + git_merge_file_result_free(&result); + git_odb_free(odb); + + return error; +} + +static int merge_conflict_resolve( + int *out, + git_merge_diff_list *diff_list, + const git_merge_diff *conflict, + const git_merge_options *merge_opts, + const git_merge_file_options *file_opts) +{ + int resolved = 0; + int error = 0; + + *out = 0; + + if ((error = merge_conflict_resolve_trivial( + &resolved, diff_list, conflict)) < 0) + goto done; + + if (!resolved && (error = merge_conflict_resolve_one_removed( + &resolved, diff_list, conflict)) < 0) + goto done; + + if (!resolved && (error = merge_conflict_resolve_one_renamed( + &resolved, diff_list, conflict)) < 0) + goto done; + + if (!resolved && (error = merge_conflict_resolve_contents( + &resolved, diff_list, conflict, merge_opts, file_opts)) < 0) + goto done; + + *out = resolved; + +done: + return error; +} + +/* Rename detection and coalescing */ + +struct merge_diff_similarity { + unsigned char similarity; + size_t other_idx; +}; + +static int index_entry_similarity_calc( + void **out, + git_repository *repo, + git_index_entry *entry, + const git_merge_options *opts) +{ + git_blob *blob; + git_diff_file diff_file; + git_object_size_t blobsize; + int error; + + if (*out || *out == &cache_invalid_marker) + return 0; + + *out = NULL; + + git_oid_clear(&diff_file.id, repo->oid_type); + + if ((error = git_blob_lookup(&blob, repo, &entry->id)) < 0) + return error; + + git_oid_cpy(&diff_file.id, &entry->id); + diff_file.path = entry->path; + diff_file.size = entry->file_size; + diff_file.mode = entry->mode; + diff_file.flags = 0; + + blobsize = git_blob_rawsize(blob); + + /* file too big for rename processing */ + if (!git__is_sizet(blobsize)) + return 0; + + error = opts->metric->buffer_signature(out, &diff_file, + git_blob_rawcontent(blob), (size_t)blobsize, + opts->metric->payload); + if (error == GIT_EBUFS) + *out = &cache_invalid_marker; + + git_blob_free(blob); + + return error; +} + +static int index_entry_similarity_inexact( + git_repository *repo, + git_index_entry *a, + size_t a_idx, + git_index_entry *b, + size_t b_idx, + void **cache, + const git_merge_options *opts) +{ + int score = 0; + int error = 0; + + if (!GIT_MODE_ISBLOB(a->mode) || !GIT_MODE_ISBLOB(b->mode)) + return 0; + + /* update signature cache if needed */ + if ((error = index_entry_similarity_calc(&cache[a_idx], repo, a, opts)) < 0 || + (error = index_entry_similarity_calc(&cache[b_idx], repo, b, opts)) < 0) + return error; + + /* some metrics may not wish to process this file (too big / too small) */ + if (cache[a_idx] == &cache_invalid_marker || cache[b_idx] == &cache_invalid_marker) + return 0; + + /* compare signatures */ + if (opts->metric->similarity(&score, cache[a_idx], cache[b_idx], opts->metric->payload) < 0) + return -1; + + /* clip score */ + if (score < 0) + score = 0; + else if (score > 100) + score = 100; + + return score; +} + +/* Tracks deletes by oid for merge_diff_mark_similarity_exact(). This is a +* non-shrinking queue where next_pos is the next position to dequeue. +*/ +typedef struct { + git_array_t(size_t) arr; + size_t next_pos; + size_t first_entry; +} deletes_by_oid_queue; + +static void deletes_by_oid_free(git_oidmap *map) { + deletes_by_oid_queue *queue; + + if (!map) + return; + + git_oidmap_foreach_value(map, queue, { + git_array_clear(queue->arr); + }); + git_oidmap_free(map); +} + +static int deletes_by_oid_enqueue(git_oidmap *map, git_pool *pool, const git_oid *id, size_t idx) +{ + deletes_by_oid_queue *queue; + size_t *array_entry; + + if ((queue = git_oidmap_get(map, id)) == NULL) { + queue = git_pool_malloc(pool, sizeof(deletes_by_oid_queue)); + GIT_ERROR_CHECK_ALLOC(queue); + + git_array_init(queue->arr); + queue->next_pos = 0; + queue->first_entry = idx; + + if (git_oidmap_set(map, id, queue) < 0) + return -1; + } else { + array_entry = git_array_alloc(queue->arr); + GIT_ERROR_CHECK_ALLOC(array_entry); + *array_entry = idx; + } + + return 0; +} + +static int deletes_by_oid_dequeue(size_t *idx, git_oidmap *map, const git_oid *id) +{ + deletes_by_oid_queue *queue; + size_t *array_entry; + + if ((queue = git_oidmap_get(map, id)) == NULL) + return GIT_ENOTFOUND; + + if (queue->next_pos == 0) { + *idx = queue->first_entry; + } else { + array_entry = git_array_get(queue->arr, queue->next_pos - 1); + if (array_entry == NULL) + return GIT_ENOTFOUND; + + *idx = *array_entry; + } + + queue->next_pos++; + return 0; +} + +static int merge_diff_mark_similarity_exact( + git_merge_diff_list *diff_list, + struct merge_diff_similarity *similarity_ours, + struct merge_diff_similarity *similarity_theirs) +{ + size_t i, j; + git_merge_diff *conflict_src, *conflict_tgt; + git_oidmap *ours_deletes_by_oid = NULL, *theirs_deletes_by_oid = NULL; + int error = 0; + + if (git_oidmap_new(&ours_deletes_by_oid) < 0 || + git_oidmap_new(&theirs_deletes_by_oid) < 0) { + error = -1; + goto done; + } + + /* Build a map of object ids to conflicts */ + git_vector_foreach(&diff_list->conflicts, i, conflict_src) { + /* Items can be the source of a rename iff they have an item in the + * ancestor slot and lack an item in the ours or theirs slot. */ + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->ancestor_entry)) + continue; + + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->our_entry)) { + error = deletes_by_oid_enqueue(ours_deletes_by_oid, &diff_list->pool, &conflict_src->ancestor_entry.id, i); + if (error < 0) + goto done; + } + + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->their_entry)) { + error = deletes_by_oid_enqueue(theirs_deletes_by_oid, &diff_list->pool, &conflict_src->ancestor_entry.id, i); + if (error < 0) + goto done; + } + } + + git_vector_foreach(&diff_list->conflicts, j, conflict_tgt) { + if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->ancestor_entry)) + continue; + + if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->our_entry)) { + if (deletes_by_oid_dequeue(&i, ours_deletes_by_oid, &conflict_tgt->our_entry.id) == 0) { + similarity_ours[i].similarity = 100; + similarity_ours[i].other_idx = j; + + similarity_ours[j].similarity = 100; + similarity_ours[j].other_idx = i; + } + } + + if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->their_entry)) { + if (deletes_by_oid_dequeue(&i, theirs_deletes_by_oid, &conflict_tgt->their_entry.id) == 0) { + similarity_theirs[i].similarity = 100; + similarity_theirs[i].other_idx = j; + + similarity_theirs[j].similarity = 100; + similarity_theirs[j].other_idx = i; + } + } + } + +done: + deletes_by_oid_free(ours_deletes_by_oid); + deletes_by_oid_free(theirs_deletes_by_oid); + + return error; +} + +static int merge_diff_mark_similarity_inexact( + git_repository *repo, + git_merge_diff_list *diff_list, + struct merge_diff_similarity *similarity_ours, + struct merge_diff_similarity *similarity_theirs, + void **cache, + const git_merge_options *opts) +{ + size_t i, j; + git_merge_diff *conflict_src, *conflict_tgt; + int similarity; + + git_vector_foreach(&diff_list->conflicts, i, conflict_src) { + /* Items can be the source of a rename iff they have an item in the + * ancestor slot and lack an item in the ours or theirs slot. */ + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->ancestor_entry) || + (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->our_entry) && + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->their_entry))) + continue; + + git_vector_foreach(&diff_list->conflicts, j, conflict_tgt) { + size_t our_idx = diff_list->conflicts.length + j; + size_t their_idx = (diff_list->conflicts.length * 2) + j; + + if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->ancestor_entry)) + continue; + + if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->our_entry) && + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->our_entry)) { + similarity = index_entry_similarity_inexact(repo, &conflict_src->ancestor_entry, i, &conflict_tgt->our_entry, our_idx, cache, opts); + + if (similarity == GIT_EBUFS) + continue; + else if (similarity < 0) + return similarity; + + if (similarity > similarity_ours[i].similarity && + similarity > similarity_ours[j].similarity) { + /* Clear previous best similarity */ + if (similarity_ours[i].similarity > 0) + similarity_ours[similarity_ours[i].other_idx].similarity = 0; + + if (similarity_ours[j].similarity > 0) + similarity_ours[similarity_ours[j].other_idx].similarity = 0; + + similarity_ours[i].similarity = similarity; + similarity_ours[i].other_idx = j; + + similarity_ours[j].similarity = similarity; + similarity_ours[j].other_idx = i; + } + } + + if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->their_entry) && + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->their_entry)) { + similarity = index_entry_similarity_inexact(repo, &conflict_src->ancestor_entry, i, &conflict_tgt->their_entry, their_idx, cache, opts); + + if (similarity > similarity_theirs[i].similarity && + similarity > similarity_theirs[j].similarity) { + /* Clear previous best similarity */ + if (similarity_theirs[i].similarity > 0) + similarity_theirs[similarity_theirs[i].other_idx].similarity = 0; + + if (similarity_theirs[j].similarity > 0) + similarity_theirs[similarity_theirs[j].other_idx].similarity = 0; + + similarity_theirs[i].similarity = similarity; + similarity_theirs[i].other_idx = j; + + similarity_theirs[j].similarity = similarity; + similarity_theirs[j].other_idx = i; + } + } + } + } + + return 0; +} + +/* + * Rename conflicts: + * + * Ancestor Ours Theirs + * + * 0a A A A No rename + * b A A* A No rename (ours was rewritten) + * c A A A* No rename (theirs rewritten) + * 1a A A B[A] Rename or rename/edit + * b A B[A] A (automergeable) + * 2 A B[A] B[A] Both renamed (automergeable) + * 3a A B[A] Rename/delete + * b A B[A] (same) + * 4a A B[A] B Rename/add [B~ours B~theirs] + * b A B B[A] (same) + * 5 A B[A] C[A] Both renamed ("1 -> 2") + * 6 A C[A] Both renamed ("2 -> 1") + * B C[B] [C~ours C~theirs] (automergeable) + */ +static void merge_diff_mark_rename_conflict( + git_merge_diff_list *diff_list, + struct merge_diff_similarity *similarity_ours, + bool ours_renamed, + size_t ours_source_idx, + struct merge_diff_similarity *similarity_theirs, + bool theirs_renamed, + size_t theirs_source_idx, + git_merge_diff *target, + const git_merge_options *opts) +{ + git_merge_diff *ours_source = NULL, *theirs_source = NULL; + + if (ours_renamed) + ours_source = diff_list->conflicts.contents[ours_source_idx]; + + if (theirs_renamed) + theirs_source = diff_list->conflicts.contents[theirs_source_idx]; + + /* Detect 2->1 conflicts */ + if (ours_renamed && theirs_renamed) { + /* Both renamed to the same target name. */ + if (ours_source_idx == theirs_source_idx) + ours_source->type = GIT_MERGE_DIFF_BOTH_RENAMED; + else { + ours_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1; + theirs_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1; + } + } else if (ours_renamed) { + /* If our source was also renamed in theirs, this is a 1->2 */ + if (similarity_theirs[ours_source_idx].similarity >= opts->rename_threshold) + ours_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_1_TO_2; + + else if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->their_entry)) { + ours_source->type = GIT_MERGE_DIFF_RENAMED_ADDED; + target->type = GIT_MERGE_DIFF_RENAMED_ADDED; + } + + else if (!GIT_MERGE_INDEX_ENTRY_EXISTS(ours_source->their_entry)) + ours_source->type = GIT_MERGE_DIFF_RENAMED_DELETED; + + else if (ours_source->type == GIT_MERGE_DIFF_MODIFIED_DELETED) + ours_source->type = GIT_MERGE_DIFF_RENAMED_MODIFIED; + } else if (theirs_renamed) { + /* If their source was also renamed in ours, this is a 1->2 */ + if (similarity_ours[theirs_source_idx].similarity >= opts->rename_threshold) + theirs_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_1_TO_2; + + else if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->our_entry)) { + theirs_source->type = GIT_MERGE_DIFF_RENAMED_ADDED; + target->type = GIT_MERGE_DIFF_RENAMED_ADDED; + } + + else if (!GIT_MERGE_INDEX_ENTRY_EXISTS(theirs_source->our_entry)) + theirs_source->type = GIT_MERGE_DIFF_RENAMED_DELETED; + + else if (theirs_source->type == GIT_MERGE_DIFF_MODIFIED_DELETED) + theirs_source->type = GIT_MERGE_DIFF_RENAMED_MODIFIED; + } +} + +GIT_INLINE(void) merge_diff_coalesce_rename( + git_index_entry *source_entry, + git_delta_t *source_status, + git_index_entry *target_entry, + git_delta_t *target_status) +{ + /* Coalesce the rename target into the rename source. */ + memcpy(source_entry, target_entry, sizeof(git_index_entry)); + *source_status = GIT_DELTA_RENAMED; + + memset(target_entry, 0x0, sizeof(git_index_entry)); + *target_status = GIT_DELTA_UNMODIFIED; +} + +static void merge_diff_list_coalesce_renames( + git_merge_diff_list *diff_list, + struct merge_diff_similarity *similarity_ours, + struct merge_diff_similarity *similarity_theirs, + const git_merge_options *opts) +{ + size_t i; + bool ours_renamed = 0, theirs_renamed = 0; + size_t ours_source_idx = 0, theirs_source_idx = 0; + git_merge_diff *ours_source, *theirs_source, *target; + + for (i = 0; i < diff_list->conflicts.length; i++) { + target = diff_list->conflicts.contents[i]; + + ours_renamed = 0; + theirs_renamed = 0; + + if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->our_entry) && + similarity_ours[i].similarity >= opts->rename_threshold) { + ours_source_idx = similarity_ours[i].other_idx; + + ours_source = diff_list->conflicts.contents[ours_source_idx]; + + merge_diff_coalesce_rename( + &ours_source->our_entry, + &ours_source->our_status, + &target->our_entry, + &target->our_status); + + similarity_ours[ours_source_idx].similarity = 0; + similarity_ours[i].similarity = 0; + + ours_renamed = 1; + } + + /* insufficient to determine direction */ + if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->their_entry) && + similarity_theirs[i].similarity >= opts->rename_threshold) { + theirs_source_idx = similarity_theirs[i].other_idx; + + theirs_source = diff_list->conflicts.contents[theirs_source_idx]; + + merge_diff_coalesce_rename( + &theirs_source->their_entry, + &theirs_source->their_status, + &target->their_entry, + &target->their_status); + + similarity_theirs[theirs_source_idx].similarity = 0; + similarity_theirs[i].similarity = 0; + + theirs_renamed = 1; + } + + merge_diff_mark_rename_conflict(diff_list, + similarity_ours, ours_renamed, ours_source_idx, + similarity_theirs, theirs_renamed, theirs_source_idx, + target, opts); + } +} + +static int merge_diff_empty(const git_vector *conflicts, size_t idx, void *p) +{ + git_merge_diff *conflict = conflicts->contents[idx]; + + GIT_UNUSED(p); + + return (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) && + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) && + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry)); +} + +static void merge_diff_list_count_candidates( + git_merge_diff_list *diff_list, + size_t *src_count, + size_t *tgt_count) +{ + git_merge_diff *entry; + size_t i; + + *src_count = 0; + *tgt_count = 0; + + git_vector_foreach(&diff_list->conflicts, i, entry) { + if (GIT_MERGE_INDEX_ENTRY_EXISTS(entry->ancestor_entry) && + (!GIT_MERGE_INDEX_ENTRY_EXISTS(entry->our_entry) || + !GIT_MERGE_INDEX_ENTRY_EXISTS(entry->their_entry))) + (*src_count)++; + else if (!GIT_MERGE_INDEX_ENTRY_EXISTS(entry->ancestor_entry)) + (*tgt_count)++; + } +} + +int git_merge_diff_list__find_renames( + git_repository *repo, + git_merge_diff_list *diff_list, + const git_merge_options *opts) +{ + struct merge_diff_similarity *similarity_ours, *similarity_theirs; + void **cache = NULL; + size_t cache_size = 0; + size_t src_count, tgt_count, i; + int error = 0; + + GIT_ASSERT_ARG(diff_list); + GIT_ASSERT_ARG(opts); + + if ((opts->flags & GIT_MERGE_FIND_RENAMES) == 0 || + !diff_list->conflicts.length) + return 0; + + similarity_ours = git__calloc(diff_list->conflicts.length, + sizeof(struct merge_diff_similarity)); + GIT_ERROR_CHECK_ALLOC(similarity_ours); + + similarity_theirs = git__calloc(diff_list->conflicts.length, + sizeof(struct merge_diff_similarity)); + GIT_ERROR_CHECK_ALLOC(similarity_theirs); + + /* Calculate similarity between items that were deleted from the ancestor + * and added in the other branch. + */ + if ((error = merge_diff_mark_similarity_exact(diff_list, similarity_ours, similarity_theirs)) < 0) + goto done; + + if (opts->rename_threshold < 100 && diff_list->conflicts.length <= opts->target_limit) { + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&cache_size, diff_list->conflicts.length, 3); + cache = git__calloc(cache_size, sizeof(void *)); + GIT_ERROR_CHECK_ALLOC(cache); + + merge_diff_list_count_candidates(diff_list, &src_count, &tgt_count); + + if (src_count > opts->target_limit || tgt_count > opts->target_limit) { + /* TODO: report! */ + } else { + if ((error = merge_diff_mark_similarity_inexact( + repo, diff_list, similarity_ours, similarity_theirs, cache, opts)) < 0) + goto done; + } + } + + /* For entries that are appropriately similar, merge the new name's entry + * into the old name. + */ + merge_diff_list_coalesce_renames(diff_list, similarity_ours, similarity_theirs, opts); + + /* And remove any entries that were merged and are now empty. */ + git_vector_remove_matching(&diff_list->conflicts, merge_diff_empty, NULL); + +done: + if (cache != NULL) { + for (i = 0; i < cache_size; ++i) { + if (cache[i] != NULL && cache[i] != &cache_invalid_marker) + opts->metric->free_signature(cache[i], opts->metric->payload); + } + + git__free(cache); + } + + git__free(similarity_ours); + git__free(similarity_theirs); + + return error; +} + +/* Directory/file conflict handling */ + +GIT_INLINE(const char *) merge_diff_path( + const git_merge_diff *conflict) +{ + if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry)) + return conflict->ancestor_entry.path; + else if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry)) + return conflict->our_entry.path; + else if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry)) + return conflict->their_entry.path; + + return NULL; +} + +GIT_INLINE(bool) merge_diff_any_side_added_or_modified( + const git_merge_diff *conflict) +{ + if (conflict->our_status == GIT_DELTA_ADDED || + conflict->our_status == GIT_DELTA_MODIFIED || + conflict->their_status == GIT_DELTA_ADDED || + conflict->their_status == GIT_DELTA_MODIFIED) + return true; + + return false; +} + +GIT_INLINE(bool) path_is_prefixed(const char *parent, const char *child) +{ + size_t child_len = strlen(child); + size_t parent_len = strlen(parent); + + if (child_len < parent_len || + strncmp(parent, child, parent_len) != 0) + return 0; + + return (child[parent_len] == '/'); +} + +GIT_INLINE(int) merge_diff_detect_df_conflict( + struct merge_diff_df_data *df_data, + git_merge_diff *conflict) +{ + const char *cur_path = merge_diff_path(conflict); + + /* Determine if this is a D/F conflict or the child of one */ + if (df_data->df_path && + path_is_prefixed(df_data->df_path, cur_path)) + conflict->type = GIT_MERGE_DIFF_DF_CHILD; + else if(df_data->df_path) + df_data->df_path = NULL; + else if (df_data->prev_path && + merge_diff_any_side_added_or_modified(df_data->prev_conflict) && + merge_diff_any_side_added_or_modified(conflict) && + path_is_prefixed(df_data->prev_path, cur_path)) { + conflict->type = GIT_MERGE_DIFF_DF_CHILD; + + df_data->prev_conflict->type = GIT_MERGE_DIFF_DIRECTORY_FILE; + df_data->df_path = df_data->prev_path; + } + + df_data->prev_path = cur_path; + df_data->prev_conflict = conflict; + + return 0; +} + +/* Conflict handling */ + +GIT_INLINE(int) merge_diff_detect_type( + git_merge_diff *conflict) +{ + if (conflict->our_status == GIT_DELTA_ADDED && + conflict->their_status == GIT_DELTA_ADDED) + conflict->type = GIT_MERGE_DIFF_BOTH_ADDED; + else if (conflict->our_status == GIT_DELTA_MODIFIED && + conflict->their_status == GIT_DELTA_MODIFIED) + conflict->type = GIT_MERGE_DIFF_BOTH_MODIFIED; + else if (conflict->our_status == GIT_DELTA_DELETED && + conflict->their_status == GIT_DELTA_DELETED) + conflict->type = GIT_MERGE_DIFF_BOTH_DELETED; + else if (conflict->our_status == GIT_DELTA_MODIFIED && + conflict->their_status == GIT_DELTA_DELETED) + conflict->type = GIT_MERGE_DIFF_MODIFIED_DELETED; + else if (conflict->our_status == GIT_DELTA_DELETED && + conflict->their_status == GIT_DELTA_MODIFIED) + conflict->type = GIT_MERGE_DIFF_MODIFIED_DELETED; + else + conflict->type = GIT_MERGE_DIFF_NONE; + + return 0; +} + +GIT_INLINE(int) index_entry_dup_pool( + git_index_entry *out, + git_pool *pool, + const git_index_entry *src) +{ + if (src != NULL) { + memcpy(out, src, sizeof(git_index_entry)); + if ((out->path = git_pool_strdup(pool, src->path)) == NULL) + return -1; + } + + return 0; +} + +GIT_INLINE(int) merge_delta_type_from_index_entries( + const git_index_entry *ancestor, + const git_index_entry *other) +{ + if (ancestor == NULL && other == NULL) + return GIT_DELTA_UNMODIFIED; + else if (ancestor == NULL && other != NULL) + return GIT_DELTA_ADDED; + else if (ancestor != NULL && other == NULL) + return GIT_DELTA_DELETED; + else if (S_ISDIR(ancestor->mode) ^ S_ISDIR(other->mode)) + return GIT_DELTA_TYPECHANGE; + else if(S_ISLNK(ancestor->mode) ^ S_ISLNK(other->mode)) + return GIT_DELTA_TYPECHANGE; + else if (git_oid__cmp(&ancestor->id, &other->id) || + ancestor->mode != other->mode) + return GIT_DELTA_MODIFIED; + + return GIT_DELTA_UNMODIFIED; +} + +static git_merge_diff *merge_diff_from_index_entries( + git_merge_diff_list *diff_list, + const git_index_entry **entries) +{ + git_merge_diff *conflict; + git_pool *pool = &diff_list->pool; + + if ((conflict = git_pool_mallocz(pool, sizeof(git_merge_diff))) == NULL) + return NULL; + + if (index_entry_dup_pool(&conflict->ancestor_entry, pool, entries[TREE_IDX_ANCESTOR]) < 0 || + index_entry_dup_pool(&conflict->our_entry, pool, entries[TREE_IDX_OURS]) < 0 || + index_entry_dup_pool(&conflict->their_entry, pool, entries[TREE_IDX_THEIRS]) < 0) + return NULL; + + conflict->our_status = merge_delta_type_from_index_entries( + entries[TREE_IDX_ANCESTOR], entries[TREE_IDX_OURS]); + conflict->their_status = merge_delta_type_from_index_entries( + entries[TREE_IDX_ANCESTOR], entries[TREE_IDX_THEIRS]); + + return conflict; +} + +/* Merge trees */ + +static int merge_diff_list_insert_conflict( + git_merge_diff_list *diff_list, + struct merge_diff_df_data *merge_df_data, + const git_index_entry *tree_items[3]) +{ + git_merge_diff *conflict; + + if ((conflict = merge_diff_from_index_entries(diff_list, tree_items)) == NULL || + merge_diff_detect_type(conflict) < 0 || + merge_diff_detect_df_conflict(merge_df_data, conflict) < 0 || + git_vector_insert(&diff_list->conflicts, conflict) < 0) + return -1; + + return 0; +} + +static int merge_diff_list_insert_unmodified( + git_merge_diff_list *diff_list, + const git_index_entry *tree_items[3]) +{ + int error = 0; + git_index_entry *entry; + + entry = git_pool_malloc(&diff_list->pool, sizeof(git_index_entry)); + GIT_ERROR_CHECK_ALLOC(entry); + + if ((error = index_entry_dup_pool(entry, &diff_list->pool, tree_items[0])) >= 0) + error = git_vector_insert(&diff_list->staged, entry); + + return error; +} + +struct merge_diff_find_data { + git_merge_diff_list *diff_list; + struct merge_diff_df_data df_data; +}; + +static int queue_difference(const git_index_entry **entries, void *data) +{ + struct merge_diff_find_data *find_data = data; + bool item_modified = false; + size_t i; + + if (!entries[0] || !entries[1] || !entries[2]) { + item_modified = true; + } else { + for (i = 1; i < 3; i++) { + if (index_entry_cmp(entries[0], entries[i]) != 0) { + item_modified = true; + break; + } + } + } + + return item_modified ? + merge_diff_list_insert_conflict( + find_data->diff_list, &find_data->df_data, entries) : + merge_diff_list_insert_unmodified(find_data->diff_list, entries); +} + +int git_merge_diff_list__find_differences( + git_merge_diff_list *diff_list, + git_iterator *ancestor_iter, + git_iterator *our_iter, + git_iterator *their_iter) +{ + git_iterator *iterators[3] = { ancestor_iter, our_iter, their_iter }; + struct merge_diff_find_data find_data = { diff_list }; + + return git_iterator_walk(iterators, 3, queue_difference, &find_data); +} + +git_merge_diff_list *git_merge_diff_list__alloc(git_repository *repo) +{ + git_merge_diff_list *diff_list = git__calloc(1, sizeof(git_merge_diff_list)); + + if (diff_list == NULL) + return NULL; + + diff_list->repo = repo; + + + if (git_pool_init(&diff_list->pool, 1) < 0 || + git_vector_init(&diff_list->staged, 0, NULL) < 0 || + git_vector_init(&diff_list->conflicts, 0, NULL) < 0 || + git_vector_init(&diff_list->resolved, 0, NULL) < 0) { + git_merge_diff_list__free(diff_list); + return NULL; + } + + return diff_list; +} + +void git_merge_diff_list__free(git_merge_diff_list *diff_list) +{ + if (!diff_list) + return; + + git_vector_free(&diff_list->staged); + git_vector_free(&diff_list->conflicts); + git_vector_free(&diff_list->resolved); + git_pool_clear(&diff_list->pool); + git__free(diff_list); +} + +static int merge_normalize_opts( + git_repository *repo, + git_merge_options *opts, + const git_merge_options *given) +{ + git_config *cfg = NULL; + git_config_entry *entry = NULL; + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(opts); + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; + + if (given != NULL) { + memcpy(opts, given, sizeof(git_merge_options)); + } else { + git_merge_options init = GIT_MERGE_OPTIONS_INIT; + memcpy(opts, &init, sizeof(init)); + } + + if ((opts->flags & GIT_MERGE_FIND_RENAMES) && !opts->rename_threshold) + opts->rename_threshold = GIT_MERGE_DEFAULT_RENAME_THRESHOLD; + + if (given && given->default_driver) { + opts->default_driver = git__strdup(given->default_driver); + GIT_ERROR_CHECK_ALLOC(opts->default_driver); + } else { + error = git_config_get_entry(&entry, cfg, "merge.default"); + + if (error == 0) { + opts->default_driver = git__strdup(entry->value); + GIT_ERROR_CHECK_ALLOC(opts->default_driver); + } else if (error == GIT_ENOTFOUND) { + error = 0; + } else { + goto done; + } + } + + if (!opts->target_limit) { + int limit = git_config__get_int_force(cfg, "merge.renamelimit", 0); + + if (!limit) + limit = git_config__get_int_force(cfg, "diff.renamelimit", 0); + + opts->target_limit = (limit <= 0) ? + GIT_MERGE_DEFAULT_TARGET_LIMIT : (unsigned int)limit; + } + + /* assign the internal metric with whitespace flag as payload */ + if (!opts->metric) { + opts->metric = git__malloc(sizeof(git_diff_similarity_metric)); + GIT_ERROR_CHECK_ALLOC(opts->metric); + + opts->metric->file_signature = git_diff_find_similar__hashsig_for_file; + opts->metric->buffer_signature = git_diff_find_similar__hashsig_for_buf; + opts->metric->free_signature = git_diff_find_similar__hashsig_free; + opts->metric->similarity = git_diff_find_similar__calc_similarity; + opts->metric->payload = (void *)GIT_HASHSIG_SMART_WHITESPACE; + } + +done: + git_config_entry_free(entry); + return error; +} + + +static int merge_index_insert_reuc( + git_index *index, + size_t idx, + const git_index_entry *entry) +{ + const git_index_reuc_entry *reuc; + int mode[3] = { 0, 0, 0 }; + git_oid const *oid[3] = { NULL, NULL, NULL }; + size_t i; + + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(*entry)) + return 0; + + if ((reuc = git_index_reuc_get_bypath(index, entry->path)) != NULL) { + for (i = 0; i < 3; i++) { + mode[i] = reuc->mode[i]; + oid[i] = &reuc->oid[i]; + } + } + + mode[idx] = entry->mode; + oid[idx] = &entry->id; + + return git_index_reuc_add(index, entry->path, + mode[0], oid[0], mode[1], oid[1], mode[2], oid[2]); +} + +static int index_update_reuc(git_index *index, git_merge_diff_list *diff_list) +{ + int error; + size_t i; + git_merge_diff *conflict; + + /* Add each entry in the resolved conflict to the REUC independently, since + * the paths may differ due to renames. */ + git_vector_foreach(&diff_list->resolved, i, conflict) { + const git_index_entry *ancestor = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ? + &conflict->ancestor_entry : NULL; + + const git_index_entry *ours = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ? + &conflict->our_entry : NULL; + + const git_index_entry *theirs = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ? + &conflict->their_entry : NULL; + + if (ancestor != NULL && + (error = merge_index_insert_reuc(index, TREE_IDX_ANCESTOR, ancestor)) < 0) + return error; + + if (ours != NULL && + (error = merge_index_insert_reuc(index, TREE_IDX_OURS, ours)) < 0) + return error; + + if (theirs != NULL && + (error = merge_index_insert_reuc(index, TREE_IDX_THEIRS, theirs)) < 0) + return error; + } + + return 0; +} + +static int index_from_diff_list( + git_index **out, + git_merge_diff_list *diff_list, + git_oid_t oid_type, + bool skip_reuc) +{ + git_index *index; + size_t i; + git_merge_diff *conflict; + int error = 0; + + *out = NULL; + + if ((error = git_index__new(&index, oid_type)) < 0) + return error; + + if ((error = git_index__fill(index, &diff_list->staged)) < 0) + goto on_error; + + git_vector_foreach(&diff_list->conflicts, i, conflict) { + const git_index_entry *ancestor = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ? + &conflict->ancestor_entry : NULL; + + const git_index_entry *ours = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ? + &conflict->our_entry : NULL; + + const git_index_entry *theirs = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ? + &conflict->their_entry : NULL; + + if ((error = git_index_conflict_add(index, ancestor, ours, theirs)) < 0) + goto on_error; + } + + /* Add each rename entry to the rename portion of the index. */ + git_vector_foreach(&diff_list->conflicts, i, conflict) { + const char *ancestor_path, *our_path, *their_path; + + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry)) + continue; + + ancestor_path = conflict->ancestor_entry.path; + + our_path = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ? + conflict->our_entry.path : NULL; + + their_path = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ? + conflict->their_entry.path : NULL; + + if ((our_path && strcmp(ancestor_path, our_path) != 0) || + (their_path && strcmp(ancestor_path, their_path) != 0)) { + if ((error = git_index_name_add(index, ancestor_path, our_path, their_path)) < 0) + goto on_error; + } + } + + if (!skip_reuc) { + if ((error = index_update_reuc(index, diff_list)) < 0) + goto on_error; + } + + *out = index; + return 0; + +on_error: + git_index_free(index); + return error; +} + +static git_iterator *iterator_given_or_empty(git_iterator **empty, git_iterator *given) +{ + git_iterator_options opts = GIT_ITERATOR_OPTIONS_INIT; + + if (given) + return given; + + opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE; + + if (git_iterator_for_nothing(empty, &opts) < 0) + return NULL; + + return *empty; +} + +int git_merge__iterators( + git_index **out, + git_repository *repo, + git_iterator *ancestor_iter, + git_iterator *our_iter, + git_iterator *theirs_iter, + const git_merge_options *given_opts) +{ + git_iterator *empty_ancestor = NULL, + *empty_ours = NULL, + *empty_theirs = NULL; + git_merge_diff_list *diff_list; + git_merge_options opts; + git_merge_file_options file_opts = GIT_MERGE_FILE_OPTIONS_INIT; + git_merge_diff *conflict; + git_vector changes; + size_t i; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + *out = NULL; + + GIT_ERROR_CHECK_VERSION( + given_opts, GIT_MERGE_OPTIONS_VERSION, "git_merge_options"); + + if ((error = merge_normalize_opts(repo, &opts, given_opts)) < 0) + return error; + + file_opts.favor = opts.file_favor; + file_opts.flags = opts.file_flags; + + /* use the git-inspired labels when virtual base building */ + if (opts.flags & GIT_MERGE_VIRTUAL_BASE) { + file_opts.ancestor_label = "merged common ancestors"; + file_opts.our_label = "Temporary merge branch 1"; + file_opts.their_label = "Temporary merge branch 2"; + file_opts.flags |= GIT_MERGE_FILE_ACCEPT_CONFLICTS; + file_opts.marker_size = GIT_MERGE_CONFLICT_MARKER_SIZE + 2; + } + + diff_list = git_merge_diff_list__alloc(repo); + GIT_ERROR_CHECK_ALLOC(diff_list); + + ancestor_iter = iterator_given_or_empty(&empty_ancestor, ancestor_iter); + our_iter = iterator_given_or_empty(&empty_ours, our_iter); + theirs_iter = iterator_given_or_empty(&empty_theirs, theirs_iter); + + if ((error = git_merge_diff_list__find_differences( + diff_list, ancestor_iter, our_iter, theirs_iter)) < 0 || + (error = git_merge_diff_list__find_renames(repo, diff_list, &opts)) < 0) + goto done; + + memcpy(&changes, &diff_list->conflicts, sizeof(git_vector)); + git_vector_clear(&diff_list->conflicts); + + git_vector_foreach(&changes, i, conflict) { + int resolved = 0; + + if ((error = merge_conflict_resolve( + &resolved, diff_list, conflict, &opts, &file_opts)) < 0) + goto done; + + if (!resolved) { + if ((opts.flags & GIT_MERGE_FAIL_ON_CONFLICT)) { + git_error_set(GIT_ERROR_MERGE, "merge conflicts exist"); + error = GIT_EMERGECONFLICT; + goto done; + } + + git_vector_insert(&diff_list->conflicts, conflict); + } + } + + error = index_from_diff_list(out, diff_list, repo->oid_type, + (opts.flags & GIT_MERGE_SKIP_REUC)); + +done: + if (!given_opts || !given_opts->metric) + git__free(opts.metric); + + git__free((char *)opts.default_driver); + + git_merge_diff_list__free(diff_list); + git_iterator_free(empty_ancestor); + git_iterator_free(empty_ours); + git_iterator_free(empty_theirs); + + return error; +} + +int git_merge_trees( + git_index **out, + git_repository *repo, + const git_tree *ancestor_tree, + const git_tree *our_tree, + const git_tree *their_tree, + const git_merge_options *merge_opts) +{ + git_iterator *ancestor_iter = NULL, *our_iter = NULL, *their_iter = NULL; + git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + /* if one side is treesame to the ancestor, take the other side */ + if (ancestor_tree && merge_opts && (merge_opts->flags & GIT_MERGE_SKIP_REUC)) { + const git_tree *result = NULL; + const git_oid *ancestor_tree_id = git_tree_id(ancestor_tree); + + if (our_tree && !git_oid_cmp(ancestor_tree_id, git_tree_id(our_tree))) + result = their_tree; + else if (their_tree && !git_oid_cmp(ancestor_tree_id, git_tree_id(their_tree))) + result = our_tree; + + if (result) { + if ((error = git_index__new(out, repo->oid_type)) == 0) + error = git_index_read_tree(*out, result); + + return error; + } + } + + iter_opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE; + + if ((error = git_iterator_for_tree( + &ancestor_iter, (git_tree *)ancestor_tree, &iter_opts)) < 0 || + (error = git_iterator_for_tree( + &our_iter, (git_tree *)our_tree, &iter_opts)) < 0 || + (error = git_iterator_for_tree( + &their_iter, (git_tree *)their_tree, &iter_opts)) < 0) + goto done; + + error = git_merge__iterators( + out, repo, ancestor_iter, our_iter, their_iter, merge_opts); + +done: + git_iterator_free(ancestor_iter); + git_iterator_free(our_iter); + git_iterator_free(their_iter); + + return error; +} + +static int merge_annotated_commits( + git_index **index_out, + git_annotated_commit **base_out, + git_repository *repo, + git_annotated_commit *our_commit, + git_annotated_commit *their_commit, + size_t recursion_level, + const git_merge_options *opts); + +GIT_INLINE(int) insert_head_ids( + git_array_oid_t *ids, + const git_annotated_commit *annotated_commit) +{ + git_oid *id; + size_t i; + + if (annotated_commit->type == GIT_ANNOTATED_COMMIT_REAL) { + id = git_array_alloc(*ids); + GIT_ERROR_CHECK_ALLOC(id); + + git_oid_cpy(id, git_commit_id(annotated_commit->commit)); + } else { + for (i = 0; i < annotated_commit->parents.size; i++) { + id = git_array_alloc(*ids); + GIT_ERROR_CHECK_ALLOC(id); + + git_oid_cpy(id, &annotated_commit->parents.ptr[i]); + } + } + + return 0; +} + +static int create_virtual_base( + git_annotated_commit **out, + git_repository *repo, + git_annotated_commit *one, + git_annotated_commit *two, + const git_merge_options *opts, + size_t recursion_level) +{ + git_annotated_commit *result = NULL; + git_index *index = NULL; + git_merge_options virtual_opts = GIT_MERGE_OPTIONS_INIT; + + /* Conflicts in the merge base creation do not propagate to conflicts + * in the result; the conflicted base will act as the common ancestor. + */ + if (opts) + memcpy(&virtual_opts, opts, sizeof(git_merge_options)); + + virtual_opts.flags &= ~GIT_MERGE_FAIL_ON_CONFLICT; + virtual_opts.flags |= GIT_MERGE_VIRTUAL_BASE; + + if ((merge_annotated_commits(&index, NULL, repo, one, two, + recursion_level + 1, &virtual_opts)) < 0) + return -1; + + result = git__calloc(1, sizeof(git_annotated_commit)); + GIT_ERROR_CHECK_ALLOC(result); + result->type = GIT_ANNOTATED_COMMIT_VIRTUAL; + result->index = index; + + if (insert_head_ids(&result->parents, one) < 0 || + insert_head_ids(&result->parents, two) < 0) { + git_annotated_commit_free(result); + return -1; + } + + *out = result; + return 0; +} + +static int compute_base( + git_annotated_commit **out, + git_repository *repo, + const git_annotated_commit *one, + const git_annotated_commit *two, + const git_merge_options *given_opts, + size_t recursion_level) +{ + git_array_oid_t head_ids = GIT_ARRAY_INIT; + git_oidarray bases = {0}; + git_annotated_commit *base = NULL, *other = NULL, *new_base = NULL; + git_merge_options opts = GIT_MERGE_OPTIONS_INIT; + size_t i, base_count; + int error; + + *out = NULL; + + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_merge_options)); + + /* With more than two commits, merge_bases_many finds the base of + * the first commit and a hypothetical merge of the others. Since + * "one" may itself be a virtual commit, which insert_head_ids + * substitutes multiple ancestors for, it needs to be added + * after "two" which is always a single real commit. + */ + if ((error = insert_head_ids(&head_ids, two)) < 0 || + (error = insert_head_ids(&head_ids, one)) < 0 || + (error = git_merge_bases_many(&bases, repo, + head_ids.size, head_ids.ptr)) < 0) + goto done; + + base_count = (opts.flags & GIT_MERGE_NO_RECURSIVE) ? 0 : bases.count; + + if (base_count) + git_oidarray__reverse(&bases); + + if ((error = git_annotated_commit_lookup(&base, repo, &bases.ids[0])) < 0) + goto done; + + for (i = 1; i < base_count; i++) { + recursion_level++; + + if (opts.recursion_limit && recursion_level > opts.recursion_limit) + break; + + if ((error = git_annotated_commit_lookup(&other, repo, + &bases.ids[i])) < 0 || + (error = create_virtual_base(&new_base, repo, base, other, &opts, + recursion_level)) < 0) + goto done; + + git_annotated_commit_free(base); + git_annotated_commit_free(other); + + base = new_base; + new_base = NULL; + other = NULL; + } + +done: + if (error == 0) + *out = base; + else + git_annotated_commit_free(base); + + git_annotated_commit_free(other); + git_annotated_commit_free(new_base); + git_oidarray_dispose(&bases); + git_array_clear(head_ids); + return error; +} + +static int iterator_for_annotated_commit( + git_iterator **out, + git_annotated_commit *commit) +{ + git_iterator_options opts = GIT_ITERATOR_OPTIONS_INIT; + int error; + + opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE; + + if (commit == NULL) { + error = git_iterator_for_nothing(out, &opts); + } else if (commit->type == GIT_ANNOTATED_COMMIT_VIRTUAL) { + error = git_iterator_for_index(out, git_index_owner(commit->index), commit->index, &opts); + } else { + if (!commit->tree && + (error = git_commit_tree(&commit->tree, commit->commit)) < 0) + goto done; + + error = git_iterator_for_tree(out, commit->tree, &opts); + } + +done: + return error; +} + +static int merge_annotated_commits( + git_index **index_out, + git_annotated_commit **base_out, + git_repository *repo, + git_annotated_commit *ours, + git_annotated_commit *theirs, + size_t recursion_level, + const git_merge_options *opts) +{ + git_annotated_commit *base = NULL; + git_iterator *base_iter = NULL, *our_iter = NULL, *their_iter = NULL; + int error; + + if ((error = compute_base(&base, repo, ours, theirs, opts, + recursion_level)) < 0) { + + if (error != GIT_ENOTFOUND) + goto done; + + git_error_clear(); + } + + if ((error = iterator_for_annotated_commit(&base_iter, base)) < 0 || + (error = iterator_for_annotated_commit(&our_iter, ours)) < 0 || + (error = iterator_for_annotated_commit(&their_iter, theirs)) < 0 || + (error = git_merge__iterators(index_out, repo, base_iter, our_iter, + their_iter, opts)) < 0) + goto done; + + if (base_out) { + *base_out = base; + base = NULL; + } + +done: + git_annotated_commit_free(base); + git_iterator_free(base_iter); + git_iterator_free(our_iter); + git_iterator_free(their_iter); + return error; +} + + +int git_merge_commits( + git_index **out, + git_repository *repo, + const git_commit *our_commit, + const git_commit *their_commit, + const git_merge_options *opts) +{ + git_annotated_commit *ours = NULL, *theirs = NULL, *base = NULL; + int error = 0; + + if ((error = git_annotated_commit_from_commit(&ours, (git_commit *)our_commit)) < 0 || + (error = git_annotated_commit_from_commit(&theirs, (git_commit *)their_commit)) < 0) + goto done; + + error = merge_annotated_commits(out, &base, repo, ours, theirs, 0, opts); + +done: + git_annotated_commit_free(ours); + git_annotated_commit_free(theirs); + git_annotated_commit_free(base); + return error; +} + +/* Merge setup / cleanup */ + +static int write_merge_head( + git_repository *repo, + const git_annotated_commit *heads[], + size_t heads_len) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_str file_path = GIT_STR_INIT; + size_t i; + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(heads); + + if ((error = git_str_joinpath(&file_path, repo->gitdir, GIT_MERGE_HEAD_FILE)) < 0 || + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_CREATE_LEADING_DIRS, GIT_MERGE_FILE_MODE)) < 0) + goto cleanup; + + for (i = 0; i < heads_len; i++) { + if ((error = git_filebuf_printf(&file, "%s\n", heads[i]->id_str)) < 0) + goto cleanup; + } + + error = git_filebuf_commit(&file); + +cleanup: + if (error < 0) + git_filebuf_cleanup(&file); + + git_str_dispose(&file_path); + + return error; +} + +static int write_merge_mode(git_repository *repo) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_str file_path = GIT_STR_INIT; + int error = 0; + + GIT_ASSERT_ARG(repo); + + if ((error = git_str_joinpath(&file_path, repo->gitdir, GIT_MERGE_MODE_FILE)) < 0 || + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_CREATE_LEADING_DIRS, GIT_MERGE_FILE_MODE)) < 0) + goto cleanup; + + if ((error = git_filebuf_write(&file, "no-ff", 5)) < 0) + goto cleanup; + + error = git_filebuf_commit(&file); + +cleanup: + if (error < 0) + git_filebuf_cleanup(&file); + + git_str_dispose(&file_path); + + return error; +} + +struct merge_msg_entry { + const git_annotated_commit *merge_head; + bool written; +}; + +static int msg_entry_is_branch( + const struct merge_msg_entry *entry, + git_vector *entries) +{ + GIT_UNUSED(entries); + + return (entry->written == 0 && + entry->merge_head->remote_url == NULL && + entry->merge_head->ref_name != NULL && + git__strncmp(GIT_REFS_HEADS_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_HEADS_DIR)) == 0); +} + +static int msg_entry_is_tracking( + const struct merge_msg_entry *entry, + git_vector *entries) +{ + GIT_UNUSED(entries); + + return (entry->written == 0 && + entry->merge_head->remote_url == NULL && + entry->merge_head->ref_name != NULL && + git__strncmp(GIT_REFS_REMOTES_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_REMOTES_DIR)) == 0); +} + +static int msg_entry_is_tag( + const struct merge_msg_entry *entry, + git_vector *entries) +{ + GIT_UNUSED(entries); + + return (entry->written == 0 && + entry->merge_head->remote_url == NULL && + entry->merge_head->ref_name != NULL && + git__strncmp(GIT_REFS_TAGS_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_TAGS_DIR)) == 0); +} + +static int msg_entry_is_remote( + const struct merge_msg_entry *entry, + git_vector *entries) +{ + if (entry->written == 0 && + entry->merge_head->remote_url != NULL && + entry->merge_head->ref_name != NULL && + git__strncmp(GIT_REFS_HEADS_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_HEADS_DIR)) == 0) + { + struct merge_msg_entry *existing; + + /* Match only branches from the same remote */ + if (entries->length == 0) + return 1; + + existing = git_vector_get(entries, 0); + + return (git__strcmp(existing->merge_head->remote_url, + entry->merge_head->remote_url) == 0); + } + + return 0; +} + +static int msg_entry_is_oid( + const struct merge_msg_entry *merge_msg_entry) +{ + return (merge_msg_entry->written == 0 && + merge_msg_entry->merge_head->ref_name == NULL && + merge_msg_entry->merge_head->remote_url == NULL); +} + +static int merge_msg_entry_written( + const struct merge_msg_entry *merge_msg_entry) +{ + return (merge_msg_entry->written == 1); +} + +static int merge_msg_entries( + git_vector *v, + const struct merge_msg_entry *entries, + size_t len, + int (*match)(const struct merge_msg_entry *entry, git_vector *entries)) +{ + size_t i; + int matches, total = 0; + + git_vector_clear(v); + + for (i = 0; i < len; i++) { + if ((matches = match(&entries[i], v)) < 0) + return matches; + else if (!matches) + continue; + + git_vector_insert(v, (struct merge_msg_entry *)&entries[i]); + total++; + } + + return total; +} + +static int merge_msg_write_entries( + git_filebuf *file, + git_vector *entries, + const char *item_name, + const char *item_plural_name, + size_t ref_name_skip, + const char *source, + char sep) +{ + struct merge_msg_entry *entry; + size_t i; + int error = 0; + + if (entries->length == 0) + return 0; + + if (sep && (error = git_filebuf_printf(file, "%c ", sep)) < 0) + goto done; + + if ((error = git_filebuf_printf(file, "%s ", + (entries->length == 1) ? item_name : item_plural_name)) < 0) + goto done; + + git_vector_foreach(entries, i, entry) { + if (i > 0 && + (error = git_filebuf_printf(file, "%s", (i == entries->length - 1) ? " and " : ", ")) < 0) + goto done; + + if ((error = git_filebuf_printf(file, "'%s'", entry->merge_head->ref_name + ref_name_skip)) < 0) + goto done; + + entry->written = 1; + } + + if (source) + error = git_filebuf_printf(file, " of %s", source); + +done: + return error; +} + +static int merge_msg_write_branches( + git_filebuf *file, + git_vector *entries, + char sep) +{ + return merge_msg_write_entries(file, entries, + "branch", "branches", strlen(GIT_REFS_HEADS_DIR), NULL, sep); +} + +static int merge_msg_write_tracking( + git_filebuf *file, + git_vector *entries, + char sep) +{ + return merge_msg_write_entries(file, entries, + "remote-tracking branch", "remote-tracking branches", 0, NULL, sep); +} + +static int merge_msg_write_tags( + git_filebuf *file, + git_vector *entries, + char sep) +{ + return merge_msg_write_entries(file, entries, + "tag", "tags", strlen(GIT_REFS_TAGS_DIR), NULL, sep); +} + +static int merge_msg_write_remotes( + git_filebuf *file, + git_vector *entries, + char sep) +{ + const char *source; + + if (entries->length == 0) + return 0; + + source = ((struct merge_msg_entry *)entries->contents[0])->merge_head->remote_url; + + return merge_msg_write_entries(file, entries, + "branch", "branches", strlen(GIT_REFS_HEADS_DIR), source, sep); +} + +static int write_merge_msg( + git_repository *repo, + const git_annotated_commit *heads[], + size_t heads_len) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_str file_path = GIT_STR_INIT; + struct merge_msg_entry *entries; + git_vector matching = GIT_VECTOR_INIT; + size_t i; + char sep = 0; + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(heads); + + entries = git__calloc(heads_len, sizeof(struct merge_msg_entry)); + GIT_ERROR_CHECK_ALLOC(entries); + + if (git_vector_init(&matching, heads_len, NULL) < 0) { + git__free(entries); + return -1; + } + + for (i = 0; i < heads_len; i++) + entries[i].merge_head = heads[i]; + + if ((error = git_str_joinpath(&file_path, repo->gitdir, GIT_MERGE_MSG_FILE)) < 0 || + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_CREATE_LEADING_DIRS, GIT_MERGE_FILE_MODE)) < 0 || + (error = git_filebuf_write(&file, "Merge ", 6)) < 0) + goto cleanup; + + /* + * This is to emulate the format of MERGE_MSG by core git. + * + * Core git will write all the commits specified by OID, in the order + * provided, until the first named branch or tag is reached, at which + * point all branches will be written in the order provided, then all + * tags, then all remote tracking branches and finally all commits that + * were specified by OID that were not already written. + * + * Yes. Really. + */ + for (i = 0; i < heads_len; i++) { + if (!msg_entry_is_oid(&entries[i])) + break; + + if ((error = git_filebuf_printf(&file, + "%scommit '%s'", (i > 0) ? "; " : "", + entries[i].merge_head->id_str)) < 0) + goto cleanup; + + entries[i].written = 1; + } + + if (i) + sep = ';'; + + if ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_branch)) < 0 || + (error = merge_msg_write_branches(&file, &matching, sep)) < 0) + goto cleanup; + + if (matching.length) + sep =','; + + if ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_tracking)) < 0 || + (error = merge_msg_write_tracking(&file, &matching, sep)) < 0) + goto cleanup; + + if (matching.length) + sep =','; + + if ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_tag)) < 0 || + (error = merge_msg_write_tags(&file, &matching, sep)) < 0) + goto cleanup; + + if (matching.length) + sep =','; + + /* We should never be called with multiple remote branches, but handle + * it in case we are... */ + while ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_remote)) > 0) { + if ((error = merge_msg_write_remotes(&file, &matching, sep)) < 0) + goto cleanup; + + if (matching.length) + sep =','; + } + + if (error < 0) + goto cleanup; + + for (i = 0; i < heads_len; i++) { + if (merge_msg_entry_written(&entries[i])) + continue; + + if ((error = git_filebuf_printf(&file, "; commit '%s'", + entries[i].merge_head->id_str)) < 0) + goto cleanup; + } + + if ((error = git_filebuf_printf(&file, "\n")) < 0 || + (error = git_filebuf_commit(&file)) < 0) + goto cleanup; + +cleanup: + if (error < 0) + git_filebuf_cleanup(&file); + + git_str_dispose(&file_path); + + git_vector_free(&matching); + git__free(entries); + + return error; +} + +int git_merge__setup( + git_repository *repo, + const git_annotated_commit *our_head, + const git_annotated_commit *heads[], + size_t heads_len) +{ + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(our_head); + GIT_ASSERT_ARG(heads); + + if ((error = git_repository__set_orig_head(repo, git_annotated_commit_id(our_head))) == 0 && + (error = write_merge_head(repo, heads, heads_len)) == 0 && + (error = write_merge_mode(repo)) == 0) { + error = write_merge_msg(repo, heads, heads_len); + } + + return error; +} + +/* Merge branches */ + +static int merge_ancestor_head( + git_annotated_commit **ancestor_head, + git_repository *repo, + const git_annotated_commit *our_head, + const git_annotated_commit **their_heads, + size_t their_heads_len) +{ + git_oid *oids, ancestor_oid; + size_t i, alloc_len; + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(our_head); + GIT_ASSERT_ARG(their_heads); + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, their_heads_len, 1); + oids = git__calloc(alloc_len, sizeof(git_oid)); + GIT_ERROR_CHECK_ALLOC(oids); + + git_oid_cpy(&oids[0], git_commit_id(our_head->commit)); + + for (i = 0; i < their_heads_len; i++) + git_oid_cpy(&oids[i + 1], git_annotated_commit_id(their_heads[i])); + + if ((error = git_merge_base_many(&ancestor_oid, repo, their_heads_len + 1, oids)) < 0) + goto on_error; + + error = git_annotated_commit_lookup(ancestor_head, repo, &ancestor_oid); + +on_error: + git__free(oids); + return error; +} + +static const char *merge_their_label(const char *branchname) +{ + const char *slash; + + if ((slash = strrchr(branchname, '/')) == NULL) + return branchname; + + if (*(slash+1) == '\0') + return "theirs"; + + return slash+1; +} + +static int merge_normalize_checkout_opts( + git_checkout_options *out, + git_repository *repo, + const git_checkout_options *given_checkout_opts, + unsigned int checkout_strategy, + git_annotated_commit *ancestor, + const git_annotated_commit *our_head, + const git_annotated_commit **their_heads, + size_t their_heads_len) +{ + git_checkout_options default_checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + int error = 0; + + GIT_UNUSED(repo); + + if (given_checkout_opts != NULL) + memcpy(out, given_checkout_opts, sizeof(git_checkout_options)); + else + memcpy(out, &default_checkout_opts, sizeof(git_checkout_options)); + + out->checkout_strategy = checkout_strategy; + + if (!out->ancestor_label) { + if (ancestor && ancestor->type == GIT_ANNOTATED_COMMIT_REAL) + out->ancestor_label = git_commit_summary(ancestor->commit); + else if (ancestor) + out->ancestor_label = "merged common ancestors"; + else + out->ancestor_label = "empty base"; + } + + if (!out->our_label) { + if (our_head && our_head->ref_name) + out->our_label = our_head->ref_name; + else + out->our_label = "ours"; + } + + if (!out->their_label) { + if (their_heads_len == 1 && their_heads[0]->ref_name) + out->their_label = merge_their_label(their_heads[0]->ref_name); + else if (their_heads_len == 1) + out->their_label = their_heads[0]->id_str; + else + out->their_label = "theirs"; + } + + return error; +} + +static int merge_check_index(size_t *conflicts, git_repository *repo, git_index *index_new, git_vector *merged_paths) +{ + git_tree *head_tree = NULL; + git_index *index_repo = NULL; + git_iterator *iter_repo = NULL, *iter_new = NULL; + git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT; + git_diff *staged_diff_list = NULL, *index_diff_list = NULL; + git_diff_delta *delta; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + git_vector staged_paths = GIT_VECTOR_INIT; + size_t i; + int error = 0; + + GIT_UNUSED(merged_paths); + + *conflicts = 0; + + /* No staged changes may exist unless the change staged is identical to + * the result of the merge. This allows one to apply to merge manually, + * then run merge. Any other staged change would be overwritten by + * a reset merge. + */ + if ((error = git_repository_head_tree(&head_tree, repo)) < 0 || + (error = git_repository_index(&index_repo, repo)) < 0 || + (error = git_diff_tree_to_index(&staged_diff_list, repo, head_tree, index_repo, &opts)) < 0) + goto done; + + if (staged_diff_list->deltas.length == 0) + goto done; + + git_vector_foreach(&staged_diff_list->deltas, i, delta) { + if ((error = git_vector_insert(&staged_paths, (char *)delta->new_file.path)) < 0) + goto done; + } + + iter_opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE; + iter_opts.pathlist.strings = (char **)staged_paths.contents; + iter_opts.pathlist.count = staged_paths.length; + + if ((error = git_iterator_for_index(&iter_repo, repo, index_repo, &iter_opts)) < 0 || + (error = git_iterator_for_index(&iter_new, repo, index_new, &iter_opts)) < 0 || + (error = git_diff__from_iterators(&index_diff_list, repo, iter_repo, iter_new, &opts)) < 0) + goto done; + + *conflicts = index_diff_list->deltas.length; + +done: + git_tree_free(head_tree); + git_index_free(index_repo); + git_iterator_free(iter_repo); + git_iterator_free(iter_new); + git_diff_free(staged_diff_list); + git_diff_free(index_diff_list); + git_vector_free(&staged_paths); + + return error; +} + +static int merge_check_workdir(size_t *conflicts, git_repository *repo, git_index *index_new, git_vector *merged_paths) +{ + git_diff *wd_diff_list = NULL; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + int error = 0; + + GIT_UNUSED(index_new); + + *conflicts = 0; + + /* We need to have merged at least 1 file for the possibility to exist to + * have conflicts with the workdir. Passing 0 as the pathspec count parameter + * will consider all files in the working directory, that is, we may detect + * a conflict if there were untracked files in the workdir prior to starting + * the merge. This typically happens when cherry-picking a commit whose + * changes have already been applied. + */ + if (merged_paths->length == 0) + return 0; + + opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED; + + /* Workdir changes may exist iff they do not conflict with changes that + * will be applied by the merge (including conflicts). Ensure that there + * are no changes in the workdir to these paths. + */ + opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH; + opts.pathspec.count = merged_paths->length; + opts.pathspec.strings = (char **)merged_paths->contents; + opts.ignore_submodules = GIT_SUBMODULE_IGNORE_ALL; + + if ((error = git_diff_index_to_workdir(&wd_diff_list, repo, NULL, &opts)) < 0) + goto done; + + *conflicts = wd_diff_list->deltas.length; + +done: + git_diff_free(wd_diff_list); + + return error; +} + +int git_merge__check_result(git_repository *repo, git_index *index_new) +{ + git_tree *head_tree = NULL; + git_iterator *iter_head = NULL, *iter_new = NULL; + git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT; + git_diff *merged_list = NULL; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + git_diff_delta *delta; + git_vector paths = GIT_VECTOR_INIT; + size_t i, index_conflicts = 0, wd_conflicts = 0, conflicts; + const git_index_entry *e; + int error = 0; + + iter_opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE; + + if ((error = git_repository_head_tree(&head_tree, repo)) < 0 || + (error = git_iterator_for_tree(&iter_head, head_tree, &iter_opts)) < 0 || + (error = git_iterator_for_index(&iter_new, repo, index_new, &iter_opts)) < 0 || + (error = git_diff__from_iterators(&merged_list, repo, iter_head, iter_new, &opts)) < 0) + goto done; + + git_vector_foreach(&merged_list->deltas, i, delta) { + if ((error = git_vector_insert(&paths, (char *)delta->new_file.path)) < 0) + goto done; + } + + for (i = 0; i < git_index_entrycount(index_new); i++) { + e = git_index_get_byindex(index_new, i); + + if (git_index_entry_is_conflict(e) && + (git_vector_last(&paths) == NULL || + strcmp(git_vector_last(&paths), e->path) != 0)) { + + if ((error = git_vector_insert(&paths, (char *)e->path)) < 0) + goto done; + } + } + + /* Make sure the index and workdir state do not prevent merging */ + if ((error = merge_check_index(&index_conflicts, repo, index_new, &paths)) < 0 || + (error = merge_check_workdir(&wd_conflicts, repo, index_new, &paths)) < 0) + goto done; + + if ((conflicts = index_conflicts + wd_conflicts) > 0) { + git_error_set(GIT_ERROR_MERGE, "%" PRIuZ " uncommitted change%s would be overwritten by merge", + conflicts, (conflicts != 1) ? "s" : ""); + error = GIT_ECONFLICT; + } + +done: + git_vector_free(&paths); + git_tree_free(head_tree); + git_iterator_free(iter_head); + git_iterator_free(iter_new); + git_diff_free(merged_list); + + return error; +} + +int git_merge__append_conflicts_to_merge_msg( + git_repository *repo, + git_index *index) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_str file_path = GIT_STR_INIT; + const char *last = NULL; + size_t i; + int error; + + if (!git_index_has_conflicts(index)) + return 0; + + if ((error = git_str_joinpath(&file_path, repo->gitdir, GIT_MERGE_MSG_FILE)) < 0 || + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_APPEND, GIT_MERGE_FILE_MODE)) < 0) + goto cleanup; + + git_filebuf_printf(&file, "\n#Conflicts:\n"); + + for (i = 0; i < git_index_entrycount(index); i++) { + const git_index_entry *e = git_index_get_byindex(index, i); + + if (!git_index_entry_is_conflict(e)) + continue; + + if (last == NULL || strcmp(e->path, last) != 0) + git_filebuf_printf(&file, "#\t%s\n", e->path); + + last = e->path; + } + + error = git_filebuf_commit(&file); + +cleanup: + if (error < 0) + git_filebuf_cleanup(&file); + + git_str_dispose(&file_path); + + return error; +} + +static int merge_state_cleanup(git_repository *repo) +{ + const char *state_files[] = { + GIT_MERGE_HEAD_FILE, + GIT_MERGE_MODE_FILE, + GIT_MERGE_MSG_FILE, + }; + + return git_repository__cleanup_files(repo, state_files, ARRAY_SIZE(state_files)); +} + +static int merge_heads( + git_annotated_commit **ancestor_head_out, + git_annotated_commit **our_head_out, + git_repository *repo, + git_reference *our_ref, + const git_annotated_commit **their_heads, + size_t their_heads_len) +{ + git_annotated_commit *ancestor_head = NULL, *our_head = NULL; + int error = 0; + + *ancestor_head_out = NULL; + *our_head_out = NULL; + + if ((error = git_annotated_commit_from_ref(&our_head, repo, our_ref)) < 0) + goto done; + + if ((error = merge_ancestor_head(&ancestor_head, repo, our_head, their_heads, their_heads_len)) < 0) { + if (error != GIT_ENOTFOUND) + goto done; + + git_error_clear(); + error = 0; + } + + *ancestor_head_out = ancestor_head; + *our_head_out = our_head; + +done: + if (error < 0) { + git_annotated_commit_free(ancestor_head); + git_annotated_commit_free(our_head); + } + + return error; +} + +static int merge_preference(git_merge_preference_t *out, git_repository *repo) +{ + git_config *config; + const char *value; + int bool_value, error = 0; + + *out = GIT_MERGE_PREFERENCE_NONE; + + if ((error = git_repository_config_snapshot(&config, repo)) < 0) + goto done; + + if ((error = git_config_get_string(&value, config, "merge.ff")) < 0) { + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = 0; + } + + goto done; + } + + if (git_config_parse_bool(&bool_value, value) == 0) { + if (!bool_value) + *out |= GIT_MERGE_PREFERENCE_NO_FASTFORWARD; + } else { + if (strcasecmp(value, "only") == 0) + *out |= GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY; + } + +done: + git_config_free(config); + return error; +} + +int git_merge_analysis_for_ref( + git_merge_analysis_t *analysis_out, + git_merge_preference_t *preference_out, + git_repository *repo, + git_reference *our_ref, + const git_annotated_commit **their_heads, + size_t their_heads_len) +{ + git_annotated_commit *ancestor_head = NULL, *our_head = NULL; + int error = 0; + bool unborn; + + GIT_ASSERT_ARG(analysis_out); + GIT_ASSERT_ARG(preference_out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(their_heads && their_heads_len > 0); + + if (their_heads_len != 1) { + git_error_set(GIT_ERROR_MERGE, "can only merge a single branch"); + error = -1; + goto done; + } + + *analysis_out = GIT_MERGE_ANALYSIS_NONE; + + if ((error = merge_preference(preference_out, repo)) < 0) + goto done; + + if ((error = git_reference__is_unborn_head(&unborn, our_ref, repo)) < 0) + goto done; + + if (unborn) { + *analysis_out |= GIT_MERGE_ANALYSIS_FASTFORWARD | GIT_MERGE_ANALYSIS_UNBORN; + error = 0; + goto done; + } + + if ((error = merge_heads(&ancestor_head, &our_head, repo, our_ref, their_heads, their_heads_len)) < 0) + goto done; + + /* We're up-to-date if we're trying to merge our own common ancestor. */ + if (ancestor_head && git_oid_equal( + git_annotated_commit_id(ancestor_head), git_annotated_commit_id(their_heads[0]))) + *analysis_out |= GIT_MERGE_ANALYSIS_UP_TO_DATE; + + /* We're fastforwardable if we're our own common ancestor. */ + else if (ancestor_head && git_oid_equal( + git_annotated_commit_id(ancestor_head), git_annotated_commit_id(our_head))) + *analysis_out |= GIT_MERGE_ANALYSIS_FASTFORWARD | GIT_MERGE_ANALYSIS_NORMAL; + + /* Otherwise, just a normal merge is possible. */ + else + *analysis_out |= GIT_MERGE_ANALYSIS_NORMAL; + +done: + git_annotated_commit_free(ancestor_head); + git_annotated_commit_free(our_head); + return error; +} + +int git_merge_analysis( + git_merge_analysis_t *analysis_out, + git_merge_preference_t *preference_out, + git_repository *repo, + const git_annotated_commit **their_heads, + size_t their_heads_len) +{ + git_reference *head_ref = NULL; + int error = 0; + + if ((error = git_reference_lookup(&head_ref, repo, GIT_HEAD_FILE)) < 0) { + git_error_set(GIT_ERROR_MERGE, "failed to lookup HEAD reference"); + return error; + } + + error = git_merge_analysis_for_ref(analysis_out, preference_out, repo, head_ref, their_heads, their_heads_len); + + git_reference_free(head_ref); + + return error; +} + +int git_merge( + git_repository *repo, + const git_annotated_commit **their_heads, + size_t their_heads_len, + const git_merge_options *merge_opts, + const git_checkout_options *given_checkout_opts) +{ + git_reference *our_ref = NULL; + git_checkout_options checkout_opts; + git_annotated_commit *our_head = NULL, *base = NULL; + git_index *repo_index = NULL, *index = NULL; + git_indexwriter indexwriter = GIT_INDEXWRITER_INIT; + unsigned int checkout_strategy; + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(their_heads && their_heads_len > 0); + + if (their_heads_len != 1) { + git_error_set(GIT_ERROR_MERGE, "can only merge a single branch"); + return -1; + } + + if ((error = git_repository__ensure_not_bare(repo, "merge")) < 0) + goto done; + + checkout_strategy = given_checkout_opts ? + given_checkout_opts->checkout_strategy : + GIT_CHECKOUT_SAFE; + + if ((error = git_indexwriter_init_for_operation(&indexwriter, repo, + &checkout_strategy)) < 0) + goto done; + + if ((error = git_repository_index(&repo_index, repo) < 0) || + (error = git_index_read(repo_index, 0) < 0)) + goto done; + + /* Write the merge setup files to the repository. */ + if ((error = git_annotated_commit_from_head(&our_head, repo)) < 0 || + (error = git_merge__setup(repo, our_head, their_heads, + their_heads_len)) < 0) + goto done; + + /* TODO: octopus */ + + if ((error = merge_annotated_commits(&index, &base, repo, our_head, + (git_annotated_commit *)their_heads[0], 0, merge_opts)) < 0 || + (error = git_merge__check_result(repo, index)) < 0 || + (error = git_merge__append_conflicts_to_merge_msg(repo, index)) < 0) + goto done; + + /* check out the merge results */ + + if ((error = merge_normalize_checkout_opts(&checkout_opts, repo, + given_checkout_opts, checkout_strategy, + base, our_head, their_heads, their_heads_len)) < 0 || + (error = git_checkout_index(repo, index, &checkout_opts)) < 0) + goto done; + + error = git_indexwriter_commit(&indexwriter); + +done: + if (error < 0) + merge_state_cleanup(repo); + + git_indexwriter_cleanup(&indexwriter); + git_index_free(index); + git_annotated_commit_free(our_head); + git_annotated_commit_free(base); + git_reference_free(our_ref); + git_index_free(repo_index); + + return error; +} + +int git_merge_options_init(git_merge_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_merge_options, GIT_MERGE_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_merge_init_options(git_merge_options *opts, unsigned int version) +{ + return git_merge_options_init(opts, version); +} +#endif + +int git_merge_file_input_init(git_merge_file_input *input, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + input, version, git_merge_file_input, GIT_MERGE_FILE_INPUT_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_merge_file_init_input(git_merge_file_input *input, unsigned int version) +{ + return git_merge_file_input_init(input, version); +} +#endif + +int git_merge_file_options_init( + git_merge_file_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_merge_file_options, GIT_MERGE_FILE_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_merge_file_init_options( + git_merge_file_options *opts, unsigned int version) +{ + return git_merge_file_options_init(opts, version); +} +#endif diff --git a/src/libgit2/merge.h b/src/libgit2/merge.h new file mode 100644 index 0000000..2393290 --- /dev/null +++ b/src/libgit2/merge.h @@ -0,0 +1,204 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_merge_h__ +#define INCLUDE_merge_h__ + +#include "common.h" + +#include "vector.h" +#include "commit_list.h" +#include "pool.h" +#include "iterator.h" + +#include "git2/types.h" +#include "git2/merge.h" +#include "git2/sys/merge.h" + +#define GIT_MERGE_MSG_FILE "MERGE_MSG" +#define GIT_MERGE_MODE_FILE "MERGE_MODE" +#define GIT_MERGE_FILE_MODE 0666 + +#define GIT_MERGE_DEFAULT_RENAME_THRESHOLD 50 +#define GIT_MERGE_DEFAULT_TARGET_LIMIT 1000 + +/** Types of changes when files are merged from branch to branch. */ +typedef enum { + /* No conflict - a change only occurs in one branch. */ + GIT_MERGE_DIFF_NONE = 0, + + /* Occurs when a file is modified in both branches. */ + GIT_MERGE_DIFF_BOTH_MODIFIED = (1 << 0), + + /* Occurs when a file is added in both branches. */ + GIT_MERGE_DIFF_BOTH_ADDED = (1 << 1), + + /* Occurs when a file is deleted in both branches. */ + GIT_MERGE_DIFF_BOTH_DELETED = (1 << 2), + + /* Occurs when a file is modified in one branch and deleted in the other. */ + GIT_MERGE_DIFF_MODIFIED_DELETED = (1 << 3), + + /* Occurs when a file is renamed in one branch and modified in the other. */ + GIT_MERGE_DIFF_RENAMED_MODIFIED = (1 << 4), + + /* Occurs when a file is renamed in one branch and deleted in the other. */ + GIT_MERGE_DIFF_RENAMED_DELETED = (1 << 5), + + /* Occurs when a file is renamed in one branch and a file with the same + * name is added in the other. Eg, A->B and new file B. Core git calls + * this a "rename/delete". */ + GIT_MERGE_DIFF_RENAMED_ADDED = (1 << 6), + + /* Occurs when both a file is renamed to the same name in the ours and + * theirs branches. Eg, A->B and A->B in both. Automergeable. */ + GIT_MERGE_DIFF_BOTH_RENAMED = (1 << 7), + + /* Occurs when a file is renamed to different names in the ours and theirs + * branches. Eg, A->B and A->C. */ + GIT_MERGE_DIFF_BOTH_RENAMED_1_TO_2 = (1 << 8), + + /* Occurs when two files are renamed to the same name in the ours and + * theirs branches. Eg, A->C and B->C. */ + GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 = (1 << 9), + + /* Occurs when an item at a path in one branch is a directory, and an + * item at the same path in a different branch is a file. */ + GIT_MERGE_DIFF_DIRECTORY_FILE = (1 << 10), + + /* The child of a folder that is in a directory/file conflict. */ + GIT_MERGE_DIFF_DF_CHILD = (1 << 11) +} git_merge_diff_t; + +typedef struct { + git_repository *repo; + git_pool pool; + + /* Vector of git_index_entry that represent the merged items that + * have been staged, either because only one side changed, or because + * the two changes were non-conflicting and mergeable. These items + * will be written as staged entries in the main index. + */ + git_vector staged; + + /* Vector of git_merge_diff entries that represent the conflicts that + * have not been automerged. These items will be written to high-stage + * entries in the main index. + */ + git_vector conflicts; + + /* Vector of git_merge_diff that have been automerged. These items + * will be written to the REUC when the index is produced. + */ + git_vector resolved; +} git_merge_diff_list; + +/** + * Description of changes to one file across three trees. + */ +typedef struct { + git_merge_diff_t type; + + git_index_entry ancestor_entry; + + git_index_entry our_entry; + git_delta_t our_status; + + git_index_entry their_entry; + git_delta_t their_status; + +} git_merge_diff; + +int git_merge__bases_many( + git_commit_list **out, + git_revwalk *walk, + git_commit_list_node *one, + git_vector *twos, + uint32_t minimum_generation); + +/* + * Three-way tree differencing + */ + +git_merge_diff_list *git_merge_diff_list__alloc(git_repository *repo); + +int git_merge_diff_list__find_differences( + git_merge_diff_list *merge_diff_list, + git_iterator *ancestor_iterator, + git_iterator *ours_iter, + git_iterator *theirs_iter); + +int git_merge_diff_list__find_renames(git_repository *repo, git_merge_diff_list *merge_diff_list, const git_merge_options *opts); + +void git_merge_diff_list__free(git_merge_diff_list *diff_list); + +/* Merge metadata setup */ + +int git_merge__setup( + git_repository *repo, + const git_annotated_commit *our_head, + const git_annotated_commit *heads[], + size_t heads_len); + +int git_merge__iterators( + git_index **out, + git_repository *repo, + git_iterator *ancestor_iter, + git_iterator *our_iter, + git_iterator *their_iter, + const git_merge_options *given_opts); + +int git_merge__check_result(git_repository *repo, git_index *index_new); + +int git_merge__append_conflicts_to_merge_msg(git_repository *repo, git_index *index); + +/* Merge files */ + +GIT_INLINE(const char *) git_merge_file__best_path( + const char *ancestor, + const char *ours, + const char *theirs) +{ + if (!ancestor) { + if (ours && theirs && strcmp(ours, theirs) == 0) + return ours; + + return NULL; + } + + if (ours && strcmp(ancestor, ours) == 0) + return theirs; + else if(theirs && strcmp(ancestor, theirs) == 0) + return ours; + + return NULL; +} + +GIT_INLINE(uint32_t) git_merge_file__best_mode( + uint32_t ancestor, uint32_t ours, uint32_t theirs) +{ + /* + * If ancestor didn't exist and either ours or theirs is executable, + * assume executable. Otherwise, if any mode changed from the ancestor, + * use that one. + */ + if (!ancestor) { + if (ours == GIT_FILEMODE_BLOB_EXECUTABLE || + theirs == GIT_FILEMODE_BLOB_EXECUTABLE) + return GIT_FILEMODE_BLOB_EXECUTABLE; + + return GIT_FILEMODE_BLOB; + } else if (ours && theirs) { + if (ancestor == ours) + return theirs; + + return ours; + } + + return 0; +} + +#endif diff --git a/src/libgit2/merge_driver.c b/src/libgit2/merge_driver.c new file mode 100644 index 0000000..19b35ac --- /dev/null +++ b/src/libgit2/merge_driver.c @@ -0,0 +1,432 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "merge_driver.h" + +#include "vector.h" +#include "runtime.h" +#include "merge.h" +#include "git2/merge.h" +#include "git2/sys/merge.h" + +static const char *merge_driver_name__text = "text"; +static const char *merge_driver_name__union = "union"; +static const char *merge_driver_name__binary = "binary"; + +struct merge_driver_registry { + git_rwlock lock; + git_vector drivers; +}; + +typedef struct { + git_merge_driver *driver; + int initialized; + char name[GIT_FLEX_ARRAY]; +} git_merge_driver_entry; + +static struct merge_driver_registry merge_driver_registry; + +static void git_merge_driver_global_shutdown(void); + +git_repository *git_merge_driver_source_repo( + const git_merge_driver_source *src) +{ + GIT_ASSERT_ARG_WITH_RETVAL(src, NULL); + return src->repo; +} + +const git_index_entry *git_merge_driver_source_ancestor( + const git_merge_driver_source *src) +{ + GIT_ASSERT_ARG_WITH_RETVAL(src, NULL); + return src->ancestor; +} + +const git_index_entry *git_merge_driver_source_ours( + const git_merge_driver_source *src) +{ + GIT_ASSERT_ARG_WITH_RETVAL(src, NULL); + return src->ours; +} + +const git_index_entry *git_merge_driver_source_theirs( + const git_merge_driver_source *src) +{ + GIT_ASSERT_ARG_WITH_RETVAL(src, NULL); + return src->theirs; +} + +const git_merge_file_options *git_merge_driver_source_file_options( + const git_merge_driver_source *src) +{ + GIT_ASSERT_ARG_WITH_RETVAL(src, NULL); + return src->file_opts; +} + +int git_merge_driver__builtin_apply( + git_merge_driver *self, + const char **path_out, + uint32_t *mode_out, + git_buf *merged_out, + const char *filter_name, + const git_merge_driver_source *src) +{ + git_merge_driver__builtin *driver = (git_merge_driver__builtin *)self; + git_merge_file_options file_opts = GIT_MERGE_FILE_OPTIONS_INIT; + git_merge_file_result result = {0}; + int error; + + GIT_UNUSED(filter_name); + + if (src->file_opts) + memcpy(&file_opts, src->file_opts, sizeof(git_merge_file_options)); + + if (driver->favor) + file_opts.favor = driver->favor; + + if ((error = git_merge_file_from_index(&result, src->repo, + src->ancestor, src->ours, src->theirs, &file_opts)) < 0) + goto done; + + if (!result.automergeable && + !(file_opts.flags & GIT_MERGE_FILE_ACCEPT_CONFLICTS)) { + error = GIT_EMERGECONFLICT; + goto done; + } + + *path_out = git_merge_file__best_path( + src->ancestor ? src->ancestor->path : NULL, + src->ours ? src->ours->path : NULL, + src->theirs ? src->theirs->path : NULL); + + *mode_out = git_merge_file__best_mode( + src->ancestor ? src->ancestor->mode : 0, + src->ours ? src->ours->mode : 0, + src->theirs ? src->theirs->mode : 0); + + merged_out->ptr = (char *)result.ptr; + merged_out->size = result.len; + merged_out->reserved = 0; + result.ptr = NULL; + +done: + git_merge_file_result_free(&result); + return error; +} + +static int merge_driver_binary_apply( + git_merge_driver *self, + const char **path_out, + uint32_t *mode_out, + git_buf *merged_out, + const char *filter_name, + const git_merge_driver_source *src) +{ + GIT_UNUSED(self); + GIT_UNUSED(path_out); + GIT_UNUSED(mode_out); + GIT_UNUSED(merged_out); + GIT_UNUSED(filter_name); + GIT_UNUSED(src); + + return GIT_EMERGECONFLICT; +} + +static int merge_driver_entry_cmp(const void *a, const void *b) +{ + const git_merge_driver_entry *entry_a = a; + const git_merge_driver_entry *entry_b = b; + + return strcmp(entry_a->name, entry_b->name); +} + +static int merge_driver_entry_search(const void *a, const void *b) +{ + const char *name_a = a; + const git_merge_driver_entry *entry_b = b; + + return strcmp(name_a, entry_b->name); +} + +git_merge_driver__builtin git_merge_driver__text = { + { + GIT_MERGE_DRIVER_VERSION, + NULL, + NULL, + git_merge_driver__builtin_apply, + }, + GIT_MERGE_FILE_FAVOR_NORMAL +}; + +git_merge_driver__builtin git_merge_driver__union = { + { + GIT_MERGE_DRIVER_VERSION, + NULL, + NULL, + git_merge_driver__builtin_apply, + }, + GIT_MERGE_FILE_FAVOR_UNION +}; + +git_merge_driver git_merge_driver__binary = { + GIT_MERGE_DRIVER_VERSION, + NULL, + NULL, + merge_driver_binary_apply +}; + +/* Note: callers must lock the registry before calling this function */ +static int merge_driver_registry_insert( + const char *name, git_merge_driver *driver) +{ + git_merge_driver_entry *entry; + + entry = git__calloc(1, sizeof(git_merge_driver_entry) + strlen(name) + 1); + GIT_ERROR_CHECK_ALLOC(entry); + + strcpy(entry->name, name); + entry->driver = driver; + + return git_vector_insert_sorted( + &merge_driver_registry.drivers, entry, NULL); +} + +int git_merge_driver_global_init(void) +{ + int error; + + if (git_rwlock_init(&merge_driver_registry.lock) < 0) + return -1; + + if ((error = git_vector_init(&merge_driver_registry.drivers, 3, + merge_driver_entry_cmp)) < 0) + goto done; + + if ((error = merge_driver_registry_insert( + merge_driver_name__text, &git_merge_driver__text.base)) < 0 || + (error = merge_driver_registry_insert( + merge_driver_name__union, &git_merge_driver__union.base)) < 0 || + (error = merge_driver_registry_insert( + merge_driver_name__binary, &git_merge_driver__binary)) < 0) + goto done; + + error = git_runtime_shutdown_register(git_merge_driver_global_shutdown); + +done: + if (error < 0) + git_vector_free_deep(&merge_driver_registry.drivers); + + return error; +} + +static void git_merge_driver_global_shutdown(void) +{ + git_merge_driver_entry *entry; + size_t i; + + if (git_rwlock_wrlock(&merge_driver_registry.lock) < 0) + return; + + git_vector_foreach(&merge_driver_registry.drivers, i, entry) { + if (entry->driver->shutdown) + entry->driver->shutdown(entry->driver); + + git__free(entry); + } + + git_vector_free(&merge_driver_registry.drivers); + + git_rwlock_wrunlock(&merge_driver_registry.lock); + git_rwlock_free(&merge_driver_registry.lock); +} + +/* Note: callers must lock the registry before calling this function */ +static int merge_driver_registry_find(size_t *pos, const char *name) +{ + return git_vector_search2(pos, &merge_driver_registry.drivers, + merge_driver_entry_search, name); +} + +/* Note: callers must lock the registry before calling this function */ +static git_merge_driver_entry *merge_driver_registry_lookup( + size_t *pos, const char *name) +{ + git_merge_driver_entry *entry = NULL; + + if (!merge_driver_registry_find(pos, name)) + entry = git_vector_get(&merge_driver_registry.drivers, *pos); + + return entry; +} + +int git_merge_driver_register(const char *name, git_merge_driver *driver) +{ + int error; + + GIT_ASSERT_ARG(name); + GIT_ASSERT_ARG(driver); + + if (git_rwlock_wrlock(&merge_driver_registry.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock merge driver registry"); + return -1; + } + + if (!merge_driver_registry_find(NULL, name)) { + git_error_set(GIT_ERROR_MERGE, "attempt to reregister existing driver '%s'", + name); + error = GIT_EEXISTS; + goto done; + } + + error = merge_driver_registry_insert(name, driver); + +done: + git_rwlock_wrunlock(&merge_driver_registry.lock); + return error; +} + +int git_merge_driver_unregister(const char *name) +{ + git_merge_driver_entry *entry; + size_t pos; + int error = 0; + + if (git_rwlock_wrlock(&merge_driver_registry.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock merge driver registry"); + return -1; + } + + if ((entry = merge_driver_registry_lookup(&pos, name)) == NULL) { + git_error_set(GIT_ERROR_MERGE, "cannot find merge driver '%s' to unregister", + name); + error = GIT_ENOTFOUND; + goto done; + } + + git_vector_remove(&merge_driver_registry.drivers, pos); + + if (entry->initialized && entry->driver->shutdown) { + entry->driver->shutdown(entry->driver); + entry->initialized = false; + } + + git__free(entry); + +done: + git_rwlock_wrunlock(&merge_driver_registry.lock); + return error; +} + +git_merge_driver *git_merge_driver_lookup(const char *name) +{ + git_merge_driver_entry *entry; + size_t pos; + int error; + + /* If we've decided the merge driver to use internally - and not + * based on user configuration (in merge_driver_name_for_path) + * then we can use a hardcoded name to compare instead of bothering + * to take a lock and look it up in the vector. + */ + if (name == merge_driver_name__text) + return &git_merge_driver__text.base; + else if (name == merge_driver_name__binary) + return &git_merge_driver__binary; + + if (git_rwlock_rdlock(&merge_driver_registry.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock merge driver registry"); + return NULL; + } + + entry = merge_driver_registry_lookup(&pos, name); + + git_rwlock_rdunlock(&merge_driver_registry.lock); + + if (entry == NULL) { + git_error_set(GIT_ERROR_MERGE, "cannot use an unregistered filter"); + return NULL; + } + + if (!entry->initialized) { + if (entry->driver->initialize && + (error = entry->driver->initialize(entry->driver)) < 0) + return NULL; + + entry->initialized = 1; + } + + return entry->driver; +} + +static int merge_driver_name_for_path( + const char **out, + git_repository *repo, + const char *path, + const char *default_driver) +{ + const char *value; + int error; + + *out = NULL; + + if ((error = git_attr_get(&value, repo, 0, path, "merge")) < 0) + return error; + + /* set: use the built-in 3-way merge driver ("text") */ + if (GIT_ATTR_IS_TRUE(value)) + *out = merge_driver_name__text; + + /* unset: do not merge ("binary") */ + else if (GIT_ATTR_IS_FALSE(value)) + *out = merge_driver_name__binary; + + else if (GIT_ATTR_IS_UNSPECIFIED(value) && default_driver) + *out = default_driver; + + else if (GIT_ATTR_IS_UNSPECIFIED(value)) + *out = merge_driver_name__text; + + else + *out = value; + + return 0; +} + + +GIT_INLINE(git_merge_driver *) merge_driver_lookup_with_wildcard( + const char *name) +{ + git_merge_driver *driver = git_merge_driver_lookup(name); + + if (driver == NULL) + driver = git_merge_driver_lookup("*"); + + return driver; +} + +int git_merge_driver_for_source( + const char **name_out, + git_merge_driver **driver_out, + const git_merge_driver_source *src) +{ + const char *path, *driver_name; + int error = 0; + + path = git_merge_file__best_path( + src->ancestor ? src->ancestor->path : NULL, + src->ours ? src->ours->path : NULL, + src->theirs ? src->theirs->path : NULL); + + if ((error = merge_driver_name_for_path( + &driver_name, src->repo, path, src->default_driver)) < 0) + return error; + + *name_out = driver_name; + *driver_out = merge_driver_lookup_with_wildcard(driver_name); + return error; +} + diff --git a/src/libgit2/merge_driver.h b/src/libgit2/merge_driver.h new file mode 100644 index 0000000..6b7da52 --- /dev/null +++ b/src/libgit2/merge_driver.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_merge_driver_h__ +#define INCLUDE_merge_driver_h__ + +#include "common.h" + +#include "git2/merge.h" +#include "git2/index.h" +#include "git2/sys/merge.h" + +struct git_merge_driver_source { + git_repository *repo; + const char *default_driver; + const git_merge_file_options *file_opts; + + const git_index_entry *ancestor; + const git_index_entry *ours; + const git_index_entry *theirs; +}; + +typedef struct git_merge_driver__builtin { + git_merge_driver base; + git_merge_file_favor_t favor; +} git_merge_driver__builtin; + +extern int git_merge_driver_global_init(void); + +extern int git_merge_driver_for_path( + char **name_out, + git_merge_driver **driver_out, + git_repository *repo, + const char *path); + +/* Merge driver configuration */ +extern int git_merge_driver_for_source( + const char **name_out, + git_merge_driver **driver_out, + const git_merge_driver_source *src); + +extern int git_merge_driver__builtin_apply( + git_merge_driver *self, + const char **path_out, + uint32_t *mode_out, + git_buf *merged_out, + const char *filter_name, + const git_merge_driver_source *src); + +/* Merge driver for text files, performs a standard three-way merge */ +extern git_merge_driver__builtin git_merge_driver__text; + +/* Merge driver for union-style merging */ +extern git_merge_driver__builtin git_merge_driver__union; + +/* Merge driver for unmergeable (binary) files: always produces conflicts */ +extern git_merge_driver git_merge_driver__binary; + +#endif diff --git a/src/libgit2/merge_file.c b/src/libgit2/merge_file.c new file mode 100644 index 0000000..ffe83cf --- /dev/null +++ b/src/libgit2/merge_file.c @@ -0,0 +1,325 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "repository.h" +#include "posix.h" +#include "futils.h" +#include "index.h" +#include "diff_xdiff.h" +#include "merge.h" + +#include "git2/repository.h" +#include "git2/object.h" +#include "git2/index.h" +#include "git2/merge.h" + +/* only examine the first 8000 bytes for binaryness. + * https://github.com/git/git/blob/77bd3ea9f54f1584147b594abc04c26ca516d987/xdiff-interface.c#L197 + */ +#define GIT_MERGE_FILE_BINARY_SIZE 8000 + +#define GIT_MERGE_FILE_SIDE_EXISTS(X) ((X)->mode != 0) + +static int merge_file_input_from_index( + git_merge_file_input *input_out, + git_odb_object **odb_object_out, + git_odb *odb, + const git_index_entry *entry) +{ + int error = 0; + + GIT_ASSERT_ARG(input_out); + GIT_ASSERT_ARG(odb_object_out); + GIT_ASSERT_ARG(odb); + GIT_ASSERT_ARG(entry); + + if ((error = git_odb_read(odb_object_out, odb, &entry->id)) < 0) + goto done; + + input_out->path = entry->path; + input_out->mode = entry->mode; + input_out->ptr = (char *)git_odb_object_data(*odb_object_out); + input_out->size = git_odb_object_size(*odb_object_out); + +done: + return error; +} + +static void merge_file_normalize_opts( + git_merge_file_options *out, + const git_merge_file_options *given_opts) +{ + if (given_opts) + memcpy(out, given_opts, sizeof(git_merge_file_options)); + else { + git_merge_file_options default_opts = GIT_MERGE_FILE_OPTIONS_INIT; + memcpy(out, &default_opts, sizeof(git_merge_file_options)); + } +} + +static int merge_file__xdiff( + git_merge_file_result *out, + const git_merge_file_input *ancestor, + const git_merge_file_input *ours, + const git_merge_file_input *theirs, + const git_merge_file_options *given_opts) +{ + xmparam_t xmparam; + mmfile_t ancestor_mmfile = {0}, our_mmfile = {0}, their_mmfile = {0}; + mmbuffer_t mmbuffer; + git_merge_file_options options = GIT_MERGE_FILE_OPTIONS_INIT; + const char *path; + int xdl_result; + int error = 0; + + memset(out, 0x0, sizeof(git_merge_file_result)); + + merge_file_normalize_opts(&options, given_opts); + + memset(&xmparam, 0x0, sizeof(xmparam_t)); + + if (ours->size > LONG_MAX || + theirs->size > LONG_MAX || + (ancestor && ancestor->size > LONG_MAX)) { + git_error_set(GIT_ERROR_MERGE, "failed to merge files"); + error = -1; + goto done; + } + + if (ancestor) { + xmparam.ancestor = (options.ancestor_label) ? + options.ancestor_label : ancestor->path; + ancestor_mmfile.ptr = (char *)ancestor->ptr; + ancestor_mmfile.size = (long)ancestor->size; + } + + xmparam.file1 = (options.our_label) ? + options.our_label : ours->path; + our_mmfile.ptr = (char *)ours->ptr; + our_mmfile.size = (long)ours->size; + + xmparam.file2 = (options.their_label) ? + options.their_label : theirs->path; + their_mmfile.ptr = (char *)theirs->ptr; + their_mmfile.size = (long)theirs->size; + + if (options.favor == GIT_MERGE_FILE_FAVOR_OURS) + xmparam.favor = XDL_MERGE_FAVOR_OURS; + else if (options.favor == GIT_MERGE_FILE_FAVOR_THEIRS) + xmparam.favor = XDL_MERGE_FAVOR_THEIRS; + else if (options.favor == GIT_MERGE_FILE_FAVOR_UNION) + xmparam.favor = XDL_MERGE_FAVOR_UNION; + + xmparam.level = (options.flags & GIT_MERGE_FILE_SIMPLIFY_ALNUM) ? + XDL_MERGE_ZEALOUS_ALNUM : XDL_MERGE_ZEALOUS; + + if (options.flags & GIT_MERGE_FILE_STYLE_DIFF3) + xmparam.style = XDL_MERGE_DIFF3; + if (options.flags & GIT_MERGE_FILE_STYLE_ZDIFF3) + xmparam.style = XDL_MERGE_ZEALOUS_DIFF3; + + if (options.flags & GIT_MERGE_FILE_IGNORE_WHITESPACE) + xmparam.xpp.flags |= XDF_IGNORE_WHITESPACE; + if (options.flags & GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE) + xmparam.xpp.flags |= XDF_IGNORE_WHITESPACE_CHANGE; + if (options.flags & GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL) + xmparam.xpp.flags |= XDF_IGNORE_WHITESPACE_AT_EOL; + + if (options.flags & GIT_MERGE_FILE_DIFF_PATIENCE) + xmparam.xpp.flags |= XDF_PATIENCE_DIFF; + + if (options.flags & GIT_MERGE_FILE_DIFF_MINIMAL) + xmparam.xpp.flags |= XDF_NEED_MINIMAL; + + xmparam.marker_size = options.marker_size; + + if ((xdl_result = xdl_merge(&ancestor_mmfile, &our_mmfile, + &their_mmfile, &xmparam, &mmbuffer)) < 0) { + git_error_set(GIT_ERROR_MERGE, "failed to merge files"); + error = -1; + goto done; + } + + path = git_merge_file__best_path( + ancestor ? ancestor->path : NULL, + ours->path, + theirs->path); + + if (path != NULL && (out->path = git__strdup(path)) == NULL) { + error = -1; + goto done; + } + + out->automergeable = (xdl_result == 0); + out->ptr = (const char *)mmbuffer.ptr; + out->len = mmbuffer.size; + out->mode = git_merge_file__best_mode( + ancestor ? ancestor->mode : 0, + ours->mode, + theirs->mode); + +done: + if (error < 0) + git_merge_file_result_free(out); + + return error; +} + +static bool merge_file__is_binary(const git_merge_file_input *file) +{ + size_t len = file ? file->size : 0; + + if (len > GIT_XDIFF_MAX_SIZE) + return true; + if (len > GIT_MERGE_FILE_BINARY_SIZE) + len = GIT_MERGE_FILE_BINARY_SIZE; + + return len ? (memchr(file->ptr, 0, len) != NULL) : false; +} + +static int merge_file__binary( + git_merge_file_result *out, + const git_merge_file_input *ours, + const git_merge_file_input *theirs, + const git_merge_file_options *given_opts) +{ + const git_merge_file_input *favored = NULL; + + memset(out, 0x0, sizeof(git_merge_file_result)); + + if (given_opts && given_opts->favor == GIT_MERGE_FILE_FAVOR_OURS) + favored = ours; + else if (given_opts && given_opts->favor == GIT_MERGE_FILE_FAVOR_THEIRS) + favored = theirs; + else + goto done; + + if ((out->path = git__strdup(favored->path)) == NULL || + (out->ptr = git__malloc(favored->size)) == NULL) + goto done; + + memcpy((char *)out->ptr, favored->ptr, favored->size); + out->len = favored->size; + out->mode = favored->mode; + out->automergeable = 1; + +done: + return 0; +} + +static int merge_file__from_inputs( + git_merge_file_result *out, + const git_merge_file_input *ancestor, + const git_merge_file_input *ours, + const git_merge_file_input *theirs, + const git_merge_file_options *given_opts) +{ + if (merge_file__is_binary(ancestor) || + merge_file__is_binary(ours) || + merge_file__is_binary(theirs)) + return merge_file__binary(out, ours, theirs, given_opts); + + return merge_file__xdiff(out, ancestor, ours, theirs, given_opts); +} + +static git_merge_file_input *git_merge_file__normalize_inputs( + git_merge_file_input *out, + const git_merge_file_input *given) +{ + memcpy(out, given, sizeof(git_merge_file_input)); + + if (!out->path) + out->path = "file.txt"; + + if (!out->mode) + out->mode = 0100644; + + return out; +} + +int git_merge_file( + git_merge_file_result *out, + const git_merge_file_input *ancestor, + const git_merge_file_input *ours, + const git_merge_file_input *theirs, + const git_merge_file_options *options) +{ + git_merge_file_input inputs[3] = { {0} }; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(ours); + GIT_ASSERT_ARG(theirs); + + memset(out, 0x0, sizeof(git_merge_file_result)); + + if (ancestor) + ancestor = git_merge_file__normalize_inputs(&inputs[0], ancestor); + + ours = git_merge_file__normalize_inputs(&inputs[1], ours); + theirs = git_merge_file__normalize_inputs(&inputs[2], theirs); + + return merge_file__from_inputs(out, ancestor, ours, theirs, options); +} + +int git_merge_file_from_index( + git_merge_file_result *out, + git_repository *repo, + const git_index_entry *ancestor, + const git_index_entry *ours, + const git_index_entry *theirs, + const git_merge_file_options *options) +{ + git_merge_file_input *ancestor_ptr = NULL, + ancestor_input = {0}, our_input = {0}, their_input = {0}; + git_odb *odb = NULL; + git_odb_object *odb_object[3] = { 0 }; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(ours); + GIT_ASSERT_ARG(theirs); + + memset(out, 0x0, sizeof(git_merge_file_result)); + + if ((error = git_repository_odb(&odb, repo)) < 0) + goto done; + + if (ancestor) { + if ((error = merge_file_input_from_index( + &ancestor_input, &odb_object[0], odb, ancestor)) < 0) + goto done; + + ancestor_ptr = &ancestor_input; + } + + if ((error = merge_file_input_from_index(&our_input, &odb_object[1], odb, ours)) < 0 || + (error = merge_file_input_from_index(&their_input, &odb_object[2], odb, theirs)) < 0) + goto done; + + error = merge_file__from_inputs(out, + ancestor_ptr, &our_input, &their_input, options); + +done: + git_odb_object_free(odb_object[0]); + git_odb_object_free(odb_object[1]); + git_odb_object_free(odb_object[2]); + git_odb_free(odb); + + return error; +} + +void git_merge_file_result_free(git_merge_file_result *result) +{ + if (result == NULL) + return; + + git__free((char *)result->path); + git__free((char *)result->ptr); +} diff --git a/src/libgit2/message.c b/src/libgit2/message.c new file mode 100644 index 0000000..ec0103a --- /dev/null +++ b/src/libgit2/message.c @@ -0,0 +1,75 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "buf.h" + +#include "git2/message.h" + +static size_t line_length_without_trailing_spaces(const char *line, size_t len) +{ + while (len) { + unsigned char c = line[len - 1]; + if (!git__isspace(c)) + break; + len--; + } + + return len; +} + +/* Greatly inspired from git.git "stripspace" */ +/* see https://github.com/git/git/blob/497215d8811ac7b8955693ceaad0899ecd894ed2/builtin/stripspace.c#L4-67 */ +static int git_message__prettify( + git_str *message_out, + const char *message, + int strip_comments, + char comment_char) +{ + const size_t message_len = strlen(message); + + int consecutive_empty_lines = 0; + size_t i, line_length, rtrimmed_line_length; + char *next_newline; + + for (i = 0; i < strlen(message); i += line_length) { + next_newline = memchr(message + i, '\n', message_len - i); + + if (next_newline != NULL) { + line_length = next_newline - (message + i) + 1; + } else { + line_length = message_len - i; + } + + if (strip_comments && line_length && message[i] == comment_char) + continue; + + rtrimmed_line_length = line_length_without_trailing_spaces(message + i, line_length); + + if (!rtrimmed_line_length) { + consecutive_empty_lines++; + continue; + } + + if (consecutive_empty_lines > 0 && message_out->size > 0) + git_str_putc(message_out, '\n'); + + consecutive_empty_lines = 0; + git_str_put(message_out, message + i, rtrimmed_line_length); + git_str_putc(message_out, '\n'); + } + + return git_str_oom(message_out) ? -1 : 0; +} + +int git_message_prettify( + git_buf *message_out, + const char *message, + int strip_comments, + char comment_char) +{ + GIT_BUF_WRAP_PRIVATE(message_out, git_message__prettify, message, strip_comments, comment_char); +} diff --git a/src/libgit2/midx.c b/src/libgit2/midx.c new file mode 100644 index 0000000..d73a1da --- /dev/null +++ b/src/libgit2/midx.c @@ -0,0 +1,928 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "midx.h" + +#include "array.h" +#include "buf.h" +#include "filebuf.h" +#include "futils.h" +#include "hash.h" +#include "odb.h" +#include "pack.h" +#include "fs_path.h" +#include "repository.h" +#include "str.h" + +#define MIDX_SIGNATURE 0x4d494458 /* "MIDX" */ +#define MIDX_VERSION 1 +#define MIDX_OBJECT_ID_VERSION 1 +struct git_midx_header { + uint32_t signature; + uint8_t version; + uint8_t object_id_version; + uint8_t chunks; + uint8_t base_midx_files; + uint32_t packfiles; +}; + +#define MIDX_PACKFILE_NAMES_ID 0x504e414d /* "PNAM" */ +#define MIDX_OID_FANOUT_ID 0x4f494446 /* "OIDF" */ +#define MIDX_OID_LOOKUP_ID 0x4f49444c /* "OIDL" */ +#define MIDX_OBJECT_OFFSETS_ID 0x4f4f4646 /* "OOFF" */ +#define MIDX_OBJECT_LARGE_OFFSETS_ID 0x4c4f4646 /* "LOFF" */ + +struct git_midx_chunk { + off64_t offset; + size_t length; +}; + +typedef int (*midx_write_cb)(const char *buf, size_t size, void *cb_data); + +static int midx_error(const char *message) +{ + git_error_set(GIT_ERROR_ODB, "invalid multi-pack-index file - %s", message); + return -1; +} + +static int midx_parse_packfile_names( + git_midx_file *idx, + const unsigned char *data, + uint32_t packfiles, + struct git_midx_chunk *chunk) +{ + int error; + uint32_t i; + char *packfile_name = (char *)(data + chunk->offset); + size_t chunk_size = chunk->length, len; + if (chunk->offset == 0) + return midx_error("missing Packfile Names chunk"); + if (chunk->length == 0) + return midx_error("empty Packfile Names chunk"); + if ((error = git_vector_init(&idx->packfile_names, packfiles, git__strcmp_cb)) < 0) + return error; + for (i = 0; i < packfiles; ++i) { + len = p_strnlen(packfile_name, chunk_size); + if (len == 0) + return midx_error("empty packfile name"); + if (len + 1 > chunk_size) + return midx_error("unterminated packfile name"); + git_vector_insert(&idx->packfile_names, packfile_name); + if (i && strcmp(git_vector_get(&idx->packfile_names, i - 1), packfile_name) >= 0) + return midx_error("packfile names are not sorted"); + if (strlen(packfile_name) <= strlen(".idx") || git__suffixcmp(packfile_name, ".idx") != 0) + return midx_error("non-.idx packfile name"); + if (strchr(packfile_name, '/') != NULL || strchr(packfile_name, '\\') != NULL) + return midx_error("non-local packfile"); + packfile_name += len + 1; + chunk_size -= len + 1; + } + return 0; +} + +static int midx_parse_oid_fanout( + git_midx_file *idx, + const unsigned char *data, + struct git_midx_chunk *chunk_oid_fanout) +{ + uint32_t i, nr; + if (chunk_oid_fanout->offset == 0) + return midx_error("missing OID Fanout chunk"); + if (chunk_oid_fanout->length == 0) + return midx_error("empty OID Fanout chunk"); + if (chunk_oid_fanout->length != 256 * 4) + return midx_error("OID Fanout chunk has wrong length"); + + idx->oid_fanout = (const uint32_t *)(data + chunk_oid_fanout->offset); + nr = 0; + for (i = 0; i < 256; ++i) { + uint32_t n = ntohl(idx->oid_fanout[i]); + if (n < nr) + return midx_error("index is non-monotonic"); + nr = n; + } + idx->num_objects = nr; + return 0; +} + +static int midx_parse_oid_lookup( + git_midx_file *idx, + const unsigned char *data, + struct git_midx_chunk *chunk_oid_lookup) +{ + size_t oid_size = git_oid_size(idx->oid_type); + + if (chunk_oid_lookup->offset == 0) + return midx_error("missing OID Lookup chunk"); + if (chunk_oid_lookup->length == 0) + return midx_error("empty OID Lookup chunk"); + if (chunk_oid_lookup->length != idx->num_objects * oid_size) + return midx_error("OID Lookup chunk has wrong length"); + + idx->oid_lookup = (unsigned char *)(data + chunk_oid_lookup->offset); + + return 0; +} + +static int midx_parse_object_offsets( + git_midx_file *idx, + const unsigned char *data, + struct git_midx_chunk *chunk_object_offsets) +{ + if (chunk_object_offsets->offset == 0) + return midx_error("missing Object Offsets chunk"); + if (chunk_object_offsets->length == 0) + return midx_error("empty Object Offsets chunk"); + if (chunk_object_offsets->length != idx->num_objects * 8) + return midx_error("Object Offsets chunk has wrong length"); + + idx->object_offsets = data + chunk_object_offsets->offset; + + return 0; +} + +static int midx_parse_object_large_offsets( + git_midx_file *idx, + const unsigned char *data, + struct git_midx_chunk *chunk_object_large_offsets) +{ + if (chunk_object_large_offsets->length == 0) + return 0; + if (chunk_object_large_offsets->length % 8 != 0) + return midx_error("malformed Object Large Offsets chunk"); + + idx->object_large_offsets = data + chunk_object_large_offsets->offset; + idx->num_object_large_offsets = chunk_object_large_offsets->length / 8; + + return 0; +} + +int git_midx_parse( + git_midx_file *idx, + const unsigned char *data, + size_t size) +{ + struct git_midx_header *hdr; + const unsigned char *chunk_hdr; + struct git_midx_chunk *last_chunk; + uint32_t i; + off64_t last_chunk_offset, chunk_offset, trailer_offset; + size_t checksum_size, oid_size; + int error; + struct git_midx_chunk chunk_packfile_names = {0}, + chunk_oid_fanout = {0}, + chunk_oid_lookup = {0}, + chunk_object_offsets = {0}, + chunk_object_large_offsets = {0}, + chunk_unknown = {0}; + + GIT_ASSERT_ARG(idx); + + oid_size = git_oid_size(idx->oid_type); + + if (size < sizeof(struct git_midx_header) + oid_size) + return midx_error("multi-pack index is too short"); + + hdr = ((struct git_midx_header *)data); + + if (hdr->signature != htonl(MIDX_SIGNATURE) || + hdr->version != MIDX_VERSION || + hdr->object_id_version != MIDX_OBJECT_ID_VERSION) { + return midx_error("unsupported multi-pack index version"); + } + if (hdr->chunks == 0) + return midx_error("no chunks in multi-pack index"); + + /* + * The very first chunk's offset should be after the header, all the chunk + * headers, and a special zero chunk. + */ + last_chunk_offset = + sizeof(struct git_midx_header) + + (1 + hdr->chunks) * 12; + + checksum_size = oid_size; + trailer_offset = size - checksum_size; + + if (trailer_offset < last_chunk_offset) + return midx_error("wrong index size"); + memcpy(idx->checksum, data + trailer_offset, checksum_size); + + chunk_hdr = data + sizeof(struct git_midx_header); + last_chunk = NULL; + for (i = 0; i < hdr->chunks; ++i, chunk_hdr += 12) { + uint32_t chunk_id = ntohl(*((uint32_t *)(chunk_hdr + 0))); + uint64_t high_offset = ((uint64_t)ntohl(*((uint32_t *)(chunk_hdr + 4)))) & 0xffffffffu; + uint64_t low_offset = ((uint64_t)ntohl(*((uint32_t *)(chunk_hdr + 8)))) & 0xffffffffu; + + if (high_offset >= INT32_MAX) + return midx_error("chunk offset out of range"); + chunk_offset = (off64_t)(high_offset << 32 | low_offset); + if (chunk_offset < last_chunk_offset) + return midx_error("chunks are non-monotonic"); + if (chunk_offset >= trailer_offset) + return midx_error("chunks extend beyond the trailer"); + if (last_chunk != NULL) + last_chunk->length = (size_t)(chunk_offset - last_chunk_offset); + last_chunk_offset = chunk_offset; + + switch (chunk_id) { + case MIDX_PACKFILE_NAMES_ID: + chunk_packfile_names.offset = last_chunk_offset; + last_chunk = &chunk_packfile_names; + break; + + case MIDX_OID_FANOUT_ID: + chunk_oid_fanout.offset = last_chunk_offset; + last_chunk = &chunk_oid_fanout; + break; + + case MIDX_OID_LOOKUP_ID: + chunk_oid_lookup.offset = last_chunk_offset; + last_chunk = &chunk_oid_lookup; + break; + + case MIDX_OBJECT_OFFSETS_ID: + chunk_object_offsets.offset = last_chunk_offset; + last_chunk = &chunk_object_offsets; + break; + + case MIDX_OBJECT_LARGE_OFFSETS_ID: + chunk_object_large_offsets.offset = last_chunk_offset; + last_chunk = &chunk_object_large_offsets; + break; + + default: + chunk_unknown.offset = last_chunk_offset; + last_chunk = &chunk_unknown; + break; + } + } + last_chunk->length = (size_t)(trailer_offset - last_chunk_offset); + + error = midx_parse_packfile_names( + idx, data, ntohl(hdr->packfiles), &chunk_packfile_names); + if (error < 0) + return error; + error = midx_parse_oid_fanout(idx, data, &chunk_oid_fanout); + if (error < 0) + return error; + error = midx_parse_oid_lookup(idx, data, &chunk_oid_lookup); + if (error < 0) + return error; + error = midx_parse_object_offsets(idx, data, &chunk_object_offsets); + if (error < 0) + return error; + error = midx_parse_object_large_offsets(idx, data, &chunk_object_large_offsets); + if (error < 0) + return error; + + return 0; +} + +int git_midx_open( + git_midx_file **idx_out, + const char *path, + git_oid_t oid_type) +{ + git_midx_file *idx; + git_file fd = -1; + size_t idx_size; + struct stat st; + int error; + + GIT_ASSERT_ARG(idx_out && path && oid_type); + + /* TODO: properly open the file without access time using O_NOATIME */ + fd = git_futils_open_ro(path); + if (fd < 0) + return fd; + + if (p_fstat(fd, &st) < 0) { + p_close(fd); + git_error_set(GIT_ERROR_ODB, "multi-pack-index file not found - '%s'", path); + return -1; + } + + if (!S_ISREG(st.st_mode) || !git__is_sizet(st.st_size)) { + p_close(fd); + git_error_set(GIT_ERROR_ODB, "invalid pack index '%s'", path); + return -1; + } + idx_size = (size_t)st.st_size; + + idx = git__calloc(1, sizeof(git_midx_file)); + GIT_ERROR_CHECK_ALLOC(idx); + + idx->oid_type = oid_type; + + error = git_str_sets(&idx->filename, path); + if (error < 0) + return error; + + error = git_futils_mmap_ro(&idx->index_map, fd, 0, idx_size); + p_close(fd); + if (error < 0) { + git_midx_free(idx); + return error; + } + + if ((error = git_midx_parse(idx, idx->index_map.data, idx_size)) < 0) { + git_midx_free(idx); + return error; + } + + *idx_out = idx; + return 0; +} + +bool git_midx_needs_refresh( + const git_midx_file *idx, + const char *path) +{ + git_file fd = -1; + struct stat st; + ssize_t bytes_read; + unsigned char checksum[GIT_HASH_MAX_SIZE]; + size_t checksum_size; + + /* TODO: properly open the file without access time using O_NOATIME */ + fd = git_futils_open_ro(path); + if (fd < 0) + return true; + + if (p_fstat(fd, &st) < 0) { + p_close(fd); + return true; + } + + if (!S_ISREG(st.st_mode) || + !git__is_sizet(st.st_size) || + (size_t)st.st_size != idx->index_map.len) { + p_close(fd); + return true; + } + + checksum_size = git_oid_size(idx->oid_type); + bytes_read = p_pread(fd, checksum, checksum_size, st.st_size - checksum_size); + p_close(fd); + + if (bytes_read != (ssize_t)checksum_size) + return true; + + return (memcmp(checksum, idx->checksum, checksum_size) != 0); +} + +int git_midx_entry_find( + git_midx_entry *e, + git_midx_file *idx, + const git_oid *short_oid, + size_t len) +{ + int pos, found = 0; + size_t pack_index, oid_size, oid_hexsize; + uint32_t hi, lo; + unsigned char *current = NULL; + const unsigned char *object_offset; + off64_t offset; + + GIT_ASSERT_ARG(idx); + + oid_size = git_oid_size(idx->oid_type); + oid_hexsize = git_oid_hexsize(idx->oid_type); + + hi = ntohl(idx->oid_fanout[(int)short_oid->id[0]]); + lo = ((short_oid->id[0] == 0x0) ? 0 : ntohl(idx->oid_fanout[(int)short_oid->id[0] - 1])); + + pos = git_pack__lookup_id(idx->oid_lookup, oid_size, lo, hi, short_oid->id, idx->oid_type); + + if (pos >= 0) { + /* An object matching exactly the oid was found */ + found = 1; + current = idx->oid_lookup + (pos * oid_size); + } else { + /* No object was found */ + /* pos refers to the object with the "closest" oid to short_oid */ + pos = -1 - pos; + if (pos < (int)idx->num_objects) { + current = idx->oid_lookup + (pos * oid_size); + + if (!git_oid_raw_ncmp(short_oid->id, current, len)) + found = 1; + } + } + + if (found && len != oid_hexsize && pos + 1 < (int)idx->num_objects) { + /* Check for ambiguousity */ + const unsigned char *next = current + oid_size; + + if (!git_oid_raw_ncmp(short_oid->id, next, len)) + found = 2; + } + + if (!found) + return git_odb__error_notfound("failed to find offset for multi-pack index entry", short_oid, len); + if (found > 1) + return git_odb__error_ambiguous("found multiple offsets for multi-pack index entry"); + + object_offset = idx->object_offsets + pos * 8; + offset = ntohl(*((uint32_t *)(object_offset + 4))); + if (idx->object_large_offsets && offset & 0x80000000) { + uint32_t object_large_offsets_pos = (uint32_t) (offset ^ 0x80000000); + const unsigned char *object_large_offsets_index = idx->object_large_offsets; + + /* Make sure we're not being sent out of bounds */ + if (object_large_offsets_pos >= idx->num_object_large_offsets) + return git_odb__error_notfound("invalid index into the object large offsets table", short_oid, len); + + object_large_offsets_index += 8 * object_large_offsets_pos; + + offset = (((uint64_t)ntohl(*((uint32_t *)(object_large_offsets_index + 0)))) << 32) | + ntohl(*((uint32_t *)(object_large_offsets_index + 4))); + } + pack_index = ntohl(*((uint32_t *)(object_offset + 0))); + if (pack_index >= git_vector_length(&idx->packfile_names)) + return midx_error("invalid index into the packfile names table"); + e->pack_index = pack_index; + e->offset = offset; + git_oid__fromraw(&e->sha1, current, idx->oid_type); + return 0; +} + +int git_midx_foreach_entry( + git_midx_file *idx, + git_odb_foreach_cb cb, + void *data) +{ + git_oid oid; + size_t oid_size, i; + int error; + + GIT_ASSERT_ARG(idx); + + oid_size = git_oid_size(idx->oid_type); + + for (i = 0; i < idx->num_objects; ++i) { + if ((error = git_oid__fromraw(&oid, &idx->oid_lookup[i * oid_size], idx->oid_type)) < 0) + return error; + + if ((error = cb(&oid, data)) != 0) + return git_error_set_after_callback(error); + } + + return error; +} + +int git_midx_close(git_midx_file *idx) +{ + GIT_ASSERT_ARG(idx); + + if (idx->index_map.data) + git_futils_mmap_free(&idx->index_map); + + git_vector_free(&idx->packfile_names); + + return 0; +} + +void git_midx_free(git_midx_file *idx) +{ + if (!idx) + return; + + git_str_dispose(&idx->filename); + git_midx_close(idx); + git__free(idx); +} + +static int packfile__cmp(const void *a_, const void *b_) +{ + const struct git_pack_file *a = a_; + const struct git_pack_file *b = b_; + + return strcmp(a->pack_name, b->pack_name); +} + +int git_midx_writer_new( + git_midx_writer **out, + const char *pack_dir +#ifdef GIT_EXPERIMENTAL_SHA256 + , git_oid_t oid_type +#endif + ) +{ + git_midx_writer *w; + +#ifndef GIT_EXPERIMENTAL_SHA256 + git_oid_t oid_type = GIT_OID_SHA1; +#endif + + GIT_ASSERT_ARG(out && pack_dir && oid_type); + + w = git__calloc(1, sizeof(git_midx_writer)); + GIT_ERROR_CHECK_ALLOC(w); + + if (git_str_sets(&w->pack_dir, pack_dir) < 0) { + git__free(w); + return -1; + } + git_fs_path_squash_slashes(&w->pack_dir); + + if (git_vector_init(&w->packs, 0, packfile__cmp) < 0) { + git_str_dispose(&w->pack_dir); + git__free(w); + return -1; + } + + w->oid_type = oid_type; + + *out = w; + return 0; +} + +void git_midx_writer_free(git_midx_writer *w) +{ + struct git_pack_file *p; + size_t i; + + if (!w) + return; + + git_vector_foreach (&w->packs, i, p) + git_mwindow_put_pack(p); + git_vector_free(&w->packs); + git_str_dispose(&w->pack_dir); + git__free(w); +} + +int git_midx_writer_add( + git_midx_writer *w, + const char *idx_path) +{ + git_str idx_path_buf = GIT_STR_INIT; + int error; + struct git_pack_file *p; + + error = git_fs_path_prettify(&idx_path_buf, idx_path, git_str_cstr(&w->pack_dir)); + if (error < 0) + return error; + + /* TODO: SHA256 */ + error = git_mwindow_get_pack(&p, git_str_cstr(&idx_path_buf), 0); + git_str_dispose(&idx_path_buf); + if (error < 0) + return error; + + error = git_vector_insert(&w->packs, p); + if (error < 0) { + git_mwindow_put_pack(p); + return error; + } + + return 0; +} + +typedef git_array_t(git_midx_entry) object_entry_array_t; + +struct object_entry_cb_state { + uint32_t pack_index; + object_entry_array_t *object_entries_array; +}; + +static int object_entry__cb(const git_oid *oid, off64_t offset, void *data) +{ + struct object_entry_cb_state *state = (struct object_entry_cb_state *)data; + + git_midx_entry *entry = git_array_alloc(*state->object_entries_array); + GIT_ERROR_CHECK_ALLOC(entry); + + git_oid_cpy(&entry->sha1, oid); + entry->offset = offset; + entry->pack_index = state->pack_index; + + return 0; +} + +static int object_entry__cmp(const void *a_, const void *b_) +{ + const git_midx_entry *a = (const git_midx_entry *)a_; + const git_midx_entry *b = (const git_midx_entry *)b_; + + return git_oid_cmp(&a->sha1, &b->sha1); +} + +static int write_offset(off64_t offset, midx_write_cb write_cb, void *cb_data) +{ + int error; + uint32_t word; + + word = htonl((uint32_t)((offset >> 32) & 0xffffffffu)); + error = write_cb((const char *)&word, sizeof(word), cb_data); + if (error < 0) + return error; + word = htonl((uint32_t)((offset >> 0) & 0xffffffffu)); + error = write_cb((const char *)&word, sizeof(word), cb_data); + if (error < 0) + return error; + + return 0; +} + +static int write_chunk_header(int chunk_id, off64_t offset, midx_write_cb write_cb, void *cb_data) +{ + uint32_t word = htonl(chunk_id); + int error = write_cb((const char *)&word, sizeof(word), cb_data); + if (error < 0) + return error; + return write_offset(offset, write_cb, cb_data); + + return 0; +} + +static int midx_write_buf(const char *buf, size_t size, void *data) +{ + git_str *b = (git_str *)data; + return git_str_put(b, buf, size); +} + +struct midx_write_hash_context { + midx_write_cb write_cb; + void *cb_data; + git_hash_ctx *ctx; +}; + +static int midx_write_hash(const char *buf, size_t size, void *data) +{ + struct midx_write_hash_context *ctx = (struct midx_write_hash_context *)data; + int error; + + error = git_hash_update(ctx->ctx, buf, size); + if (error < 0) + return error; + + return ctx->write_cb(buf, size, ctx->cb_data); +} + +static int midx_write( + git_midx_writer *w, + midx_write_cb write_cb, + void *cb_data) +{ + int error = 0; + size_t i; + struct git_pack_file *p; + struct git_midx_header hdr = {0}; + uint32_t oid_fanout_count; + uint32_t object_large_offsets_count; + uint32_t oid_fanout[256]; + off64_t offset; + git_str packfile_names = GIT_STR_INIT, + oid_lookup = GIT_STR_INIT, + object_offsets = GIT_STR_INIT, + object_large_offsets = GIT_STR_INIT; + unsigned char checksum[GIT_HASH_MAX_SIZE]; + size_t checksum_size, oid_size; + git_midx_entry *entry; + object_entry_array_t object_entries_array = GIT_ARRAY_INIT; + git_vector object_entries = GIT_VECTOR_INIT; + git_hash_ctx ctx; + git_hash_algorithm_t checksum_type; + struct midx_write_hash_context hash_cb_data = {0}; + + hdr.signature = htonl(MIDX_SIGNATURE); + hdr.version = MIDX_VERSION; + hdr.object_id_version = MIDX_OBJECT_ID_VERSION; + hdr.base_midx_files = 0; + + hash_cb_data.write_cb = write_cb; + hash_cb_data.cb_data = cb_data; + hash_cb_data.ctx = &ctx; + + oid_size = git_oid_size(w->oid_type); + + GIT_ASSERT((checksum_type = git_oid_algorithm(w->oid_type))); + checksum_size = git_hash_size(checksum_type); + + if ((error = git_hash_ctx_init(&ctx, checksum_type)) < 0) + return error; + + cb_data = &hash_cb_data; + write_cb = midx_write_hash; + + git_vector_sort(&w->packs); + git_vector_foreach (&w->packs, i, p) { + git_str relative_index = GIT_STR_INIT; + struct object_entry_cb_state state = {0}; + size_t path_len; + + state.pack_index = (uint32_t)i; + state.object_entries_array = &object_entries_array; + + error = git_str_sets(&relative_index, p->pack_name); + if (error < 0) + goto cleanup; + error = git_fs_path_make_relative(&relative_index, git_str_cstr(&w->pack_dir)); + if (error < 0) { + git_str_dispose(&relative_index); + goto cleanup; + } + path_len = git_str_len(&relative_index); + if (path_len <= strlen(".pack") || git__suffixcmp(git_str_cstr(&relative_index), ".pack") != 0) { + git_str_dispose(&relative_index); + git_error_set(GIT_ERROR_INVALID, "invalid packfile name: '%s'", p->pack_name); + error = -1; + goto cleanup; + } + path_len -= strlen(".pack"); + + git_str_put(&packfile_names, git_str_cstr(&relative_index), path_len); + git_str_puts(&packfile_names, ".idx"); + git_str_putc(&packfile_names, '\0'); + git_str_dispose(&relative_index); + + error = git_pack_foreach_entry_offset(p, object_entry__cb, &state); + if (error < 0) + goto cleanup; + } + + /* Sort the object entries. */ + error = git_vector_init(&object_entries, git_array_size(object_entries_array), object_entry__cmp); + if (error < 0) + goto cleanup; + git_array_foreach (object_entries_array, i, entry) { + if ((error = git_vector_set(NULL, &object_entries, i, entry)) < 0) + goto cleanup; + } + git_vector_set_sorted(&object_entries, 0); + git_vector_sort(&object_entries); + git_vector_uniq(&object_entries, NULL); + + /* Pad the packfile names so it is a multiple of four. */ + while (git_str_len(&packfile_names) & 3) + git_str_putc(&packfile_names, '\0'); + + /* Fill the OID Fanout table. */ + oid_fanout_count = 0; + for (i = 0; i < 256; i++) { + while (oid_fanout_count < git_vector_length(&object_entries) && + ((const git_midx_entry *)git_vector_get(&object_entries, oid_fanout_count))->sha1.id[0] <= i) + ++oid_fanout_count; + oid_fanout[i] = htonl(oid_fanout_count); + } + + /* Fill the OID Lookup table. */ + git_vector_foreach (&object_entries, i, entry) { + error = git_str_put(&oid_lookup, + (char *)&entry->sha1.id, oid_size); + + if (error < 0) + goto cleanup; + } + + /* Fill the Object Offsets and Object Large Offsets tables. */ + object_large_offsets_count = 0; + git_vector_foreach (&object_entries, i, entry) { + uint32_t word; + + word = htonl((uint32_t)entry->pack_index); + error = git_str_put(&object_offsets, (const char *)&word, sizeof(word)); + if (error < 0) + goto cleanup; + if (entry->offset >= 0x80000000l) { + word = htonl(0x80000000u | object_large_offsets_count++); + if ((error = write_offset(entry->offset, midx_write_buf, &object_large_offsets)) < 0) + goto cleanup; + } else { + word = htonl((uint32_t)entry->offset & 0x7fffffffu); + } + + error = git_str_put(&object_offsets, (const char *)&word, sizeof(word)); + if (error < 0) + goto cleanup; + } + + /* Write the header. */ + hdr.packfiles = htonl((uint32_t)git_vector_length(&w->packs)); + hdr.chunks = 4; + if (git_str_len(&object_large_offsets) > 0) + hdr.chunks++; + error = write_cb((const char *)&hdr, sizeof(hdr), cb_data); + if (error < 0) + goto cleanup; + + /* Write the chunk headers. */ + offset = sizeof(hdr) + (hdr.chunks + 1) * 12; + error = write_chunk_header(MIDX_PACKFILE_NAMES_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += git_str_len(&packfile_names); + error = write_chunk_header(MIDX_OID_FANOUT_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += sizeof(oid_fanout); + error = write_chunk_header(MIDX_OID_LOOKUP_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += git_str_len(&oid_lookup); + error = write_chunk_header(MIDX_OBJECT_OFFSETS_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += git_str_len(&object_offsets); + if (git_str_len(&object_large_offsets) > 0) { + error = write_chunk_header(MIDX_OBJECT_LARGE_OFFSETS_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += git_str_len(&object_large_offsets); + } + error = write_chunk_header(0, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + + /* Write all the chunks. */ + error = write_cb(git_str_cstr(&packfile_names), git_str_len(&packfile_names), cb_data); + if (error < 0) + goto cleanup; + error = write_cb((const char *)oid_fanout, sizeof(oid_fanout), cb_data); + if (error < 0) + goto cleanup; + error = write_cb(git_str_cstr(&oid_lookup), git_str_len(&oid_lookup), cb_data); + if (error < 0) + goto cleanup; + error = write_cb(git_str_cstr(&object_offsets), git_str_len(&object_offsets), cb_data); + if (error < 0) + goto cleanup; + error = write_cb(git_str_cstr(&object_large_offsets), git_str_len(&object_large_offsets), cb_data); + if (error < 0) + goto cleanup; + + /* Finalize the checksum and write the trailer. */ + error = git_hash_final(checksum, &ctx); + if (error < 0) + goto cleanup; + error = write_cb((char *)checksum, checksum_size, cb_data); + if (error < 0) + goto cleanup; + +cleanup: + git_array_clear(object_entries_array); + git_vector_free(&object_entries); + git_str_dispose(&packfile_names); + git_str_dispose(&oid_lookup); + git_str_dispose(&object_offsets); + git_str_dispose(&object_large_offsets); + git_hash_ctx_cleanup(&ctx); + return error; +} + +static int midx_write_filebuf(const char *buf, size_t size, void *data) +{ + git_filebuf *f = (git_filebuf *)data; + return git_filebuf_write(f, buf, size); +} + +int git_midx_writer_commit( + git_midx_writer *w) +{ + int error; + int filebuf_flags = GIT_FILEBUF_DO_NOT_BUFFER; + git_str midx_path = GIT_STR_INIT; + git_filebuf output = GIT_FILEBUF_INIT; + + error = git_str_joinpath(&midx_path, git_str_cstr(&w->pack_dir), "multi-pack-index"); + if (error < 0) + return error; + + if (git_repository__fsync_gitdir) + filebuf_flags |= GIT_FILEBUF_FSYNC; + error = git_filebuf_open(&output, git_str_cstr(&midx_path), filebuf_flags, 0644); + git_str_dispose(&midx_path); + if (error < 0) + return error; + + error = midx_write(w, midx_write_filebuf, &output); + if (error < 0) { + git_filebuf_cleanup(&output); + return error; + } + + return git_filebuf_commit(&output); +} + +int git_midx_writer_dump( + git_buf *midx, + git_midx_writer *w) +{ + git_str str = GIT_STR_INIT; + int error; + + if ((error = git_buf_tostr(&str, midx)) < 0 || + (error = midx_write(w, midx_write_buf, &str)) == 0) + error = git_buf_fromstr(midx, &str); + + git_str_dispose(&str); + return error; +} diff --git a/src/libgit2/midx.h b/src/libgit2/midx.h new file mode 100644 index 0000000..5107f69 --- /dev/null +++ b/src/libgit2/midx.h @@ -0,0 +1,121 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_midx_h__ +#define INCLUDE_midx_h__ + +#include "common.h" + +#include + +#include "git2/sys/midx.h" + +#include "map.h" +#include "mwindow.h" +#include "odb.h" +#include "oid.h" + +/* + * A multi-pack-index file. + * + * This file contains a merged index for multiple independent .pack files. This + * can help speed up locating objects without requiring a garbage collection + * cycle to create a single .pack file. + * + * Support for this feature was added in git 2.21, and requires the + * `core.multiPackIndex` config option to be set. + */ +typedef struct git_midx_file { + git_map index_map; + + /* The table of Packfile Names. */ + git_vector packfile_names; + + /* The OID Fanout table. */ + const uint32_t *oid_fanout; + /* The total number of objects in the index. */ + uint32_t num_objects; + + /* The OID Lookup table. */ + unsigned char *oid_lookup; + + /* The Object Offsets table. Each entry has two 4-byte fields with the pack index and the offset. */ + const unsigned char *object_offsets; + + /* The Object Large Offsets table. */ + const unsigned char *object_large_offsets; + /* The number of entries in the Object Large Offsets table. Each entry has an 8-byte with an offset */ + size_t num_object_large_offsets; + + /* + * The trailer of the file. Contains the checksum of the whole + * file, in the repository's object format hash. + */ + unsigned char checksum[GIT_HASH_MAX_SIZE]; + + /* The type of object IDs in the midx. */ + git_oid_t oid_type; + + /* something like ".git/objects/pack/multi-pack-index". */ + git_str filename; +} git_midx_file; + +/* + * An entry in the multi-pack-index file. Similar in purpose to git_pack_entry. + */ +typedef struct git_midx_entry { + /* The index within idx->packfile_names where the packfile name can be found. */ + size_t pack_index; + /* The offset within the .pack file where the requested object is found. */ + off64_t offset; + /* The SHA-1 hash of the requested object. */ + git_oid sha1; +} git_midx_entry; + +/* + * A writer for `multi-pack-index` files. + */ +struct git_midx_writer { + /* + * The path of the directory where the .pack/.idx files are stored. The + * `multi-pack-index` file will be written to the same directory. + */ + git_str pack_dir; + + /* The list of `git_pack_file`s. */ + git_vector packs; + + /* The object ID type of the writer. */ + git_oid_t oid_type; +}; + +int git_midx_open( + git_midx_file **idx_out, + const char *path, + git_oid_t oid_type); +bool git_midx_needs_refresh( + const git_midx_file *idx, + const char *path); +int git_midx_entry_find( + git_midx_entry *e, + git_midx_file *idx, + const git_oid *short_oid, + size_t len); +int git_midx_foreach_entry( + git_midx_file *idx, + git_odb_foreach_cb cb, + void *data); +int git_midx_close(git_midx_file *idx); +void git_midx_free(git_midx_file *idx); + +/* This is exposed for use in the fuzzers. */ +int git_midx_parse( + git_midx_file *idx, + const unsigned char *data, + size_t size); + +#endif diff --git a/src/libgit2/mwindow.c b/src/libgit2/mwindow.c new file mode 100644 index 0000000..b8295d9 --- /dev/null +++ b/src/libgit2/mwindow.c @@ -0,0 +1,544 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "mwindow.h" + +#include "vector.h" +#include "futils.h" +#include "map.h" +#include "runtime.h" +#include "strmap.h" +#include "pack.h" + +#define DEFAULT_WINDOW_SIZE \ + (sizeof(void*) >= 8 \ + ? 1 * 1024 * 1024 * 1024 \ + : 32 * 1024 * 1024) + +#define DEFAULT_MAPPED_LIMIT \ + ((1024 * 1024) * (sizeof(void*) >= 8 ? UINT64_C(8192) : UINT64_C(256))) + +/* default is unlimited */ +#define DEFAULT_FILE_LIMIT 0 + +size_t git_mwindow__window_size = DEFAULT_WINDOW_SIZE; +size_t git_mwindow__mapped_limit = DEFAULT_MAPPED_LIMIT; +size_t git_mwindow__file_limit = DEFAULT_FILE_LIMIT; + +/* Mutex to control access to `git_mwindow__mem_ctl` and `git__pack_cache`. */ +git_mutex git__mwindow_mutex; + +/* Whenever you want to read or modify this, grab `git__mwindow_mutex` */ +git_mwindow_ctl git_mwindow__mem_ctl; + +/* Global list of mwindow files, to open packs once across repos */ +git_strmap *git__pack_cache = NULL; + +static void git_mwindow_global_shutdown(void) +{ + git_strmap *tmp = git__pack_cache; + + git_mutex_free(&git__mwindow_mutex); + + git__pack_cache = NULL; + git_strmap_free(tmp); +} + +int git_mwindow_global_init(void) +{ + int error; + + GIT_ASSERT(!git__pack_cache); + + if ((error = git_mutex_init(&git__mwindow_mutex)) < 0 || + (error = git_strmap_new(&git__pack_cache)) < 0) + return error; + + return git_runtime_shutdown_register(git_mwindow_global_shutdown); +} + +int git_mwindow_get_pack( + struct git_pack_file **out, + const char *path, + git_oid_t oid_type) +{ + struct git_pack_file *pack; + char *packname; + int error; + + if ((error = git_packfile__name(&packname, path)) < 0) + return error; + + if (git_mutex_lock(&git__mwindow_mutex) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock mwindow mutex"); + return -1; + } + + pack = git_strmap_get(git__pack_cache, packname); + git__free(packname); + + if (pack != NULL) { + git_atomic32_inc(&pack->refcount); + git_mutex_unlock(&git__mwindow_mutex); + *out = pack; + return 0; + } + + /* If we didn't find it, we need to create it */ + if ((error = git_packfile_alloc(&pack, path, oid_type)) < 0) { + git_mutex_unlock(&git__mwindow_mutex); + return error; + } + + git_atomic32_inc(&pack->refcount); + + error = git_strmap_set(git__pack_cache, pack->pack_name, pack); + git_mutex_unlock(&git__mwindow_mutex); + if (error < 0) { + git_packfile_free(pack, false); + return error; + } + + *out = pack; + return 0; +} + +int git_mwindow_put_pack(struct git_pack_file *pack) +{ + int count, error; + struct git_pack_file *pack_to_delete = NULL; + + if ((error = git_mutex_lock(&git__mwindow_mutex)) < 0) + return error; + + /* put before get would be a corrupted state */ + GIT_ASSERT(git__pack_cache); + + /* if we cannot find it, the state is corrupted */ + GIT_ASSERT(git_strmap_exists(git__pack_cache, pack->pack_name)); + + count = git_atomic32_dec(&pack->refcount); + if (count == 0) { + git_strmap_delete(git__pack_cache, pack->pack_name); + pack_to_delete = pack; + } + git_mutex_unlock(&git__mwindow_mutex); + git_packfile_free(pack_to_delete, false); + + return 0; +} + +/* + * Free all the windows in a sequence, typically because we're done + * with the file. Needs to hold the git__mwindow_mutex. + */ +static int git_mwindow_free_all_locked(git_mwindow_file *mwf) +{ + git_mwindow_ctl *ctl = &git_mwindow__mem_ctl; + size_t i; + + /* + * Remove these windows from the global list + */ + for (i = 0; i < ctl->windowfiles.length; ++i){ + if (git_vector_get(&ctl->windowfiles, i) == mwf) { + git_vector_remove(&ctl->windowfiles, i); + break; + } + } + + if (ctl->windowfiles.length == 0) { + git_vector_free(&ctl->windowfiles); + ctl->windowfiles.contents = NULL; + } + + while (mwf->windows) { + git_mwindow *w = mwf->windows; + GIT_ASSERT(w->inuse_cnt == 0); + + ctl->mapped -= w->window_map.len; + ctl->open_windows--; + + git_futils_mmap_free(&w->window_map); + + mwf->windows = w->next; + git__free(w); + } + + return 0; +} + +int git_mwindow_free_all(git_mwindow_file *mwf) +{ + int error; + + if (git_mutex_lock(&git__mwindow_mutex)) { + git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex"); + return -1; + } + + error = git_mwindow_free_all_locked(mwf); + + git_mutex_unlock(&git__mwindow_mutex); + + return error; +} + +/* + * Check if a window 'win' contains both the address 'offset' and 'extra'. + * + * 'extra' is the size of the hash we're using as we always want to make sure + * that it's contained. + */ +int git_mwindow_contains(git_mwindow *win, off64_t offset, off64_t extra) +{ + off64_t win_off = win->offset; + return win_off <= offset + && (offset + extra) <= (off64_t)(win_off + win->window_map.len); +} + +#define GIT_MWINDOW__LRU -1 +#define GIT_MWINDOW__MRU 1 + +/* + * Find the least- or most-recently-used window in a file that is not currently + * being used. The 'only_unused' flag controls whether the caller requires the + * file to only have unused windows. If '*out_window' is non-null, it is used as + * a starting point for the comparison. + * + * Returns whether such a window was found in the file. + */ +static bool git_mwindow_scan_recently_used( + git_mwindow_file *mwf, + git_mwindow **out_window, + git_mwindow **out_last, + bool only_unused, + int comparison_sign) +{ + git_mwindow *w, *w_last; + git_mwindow *lru_window = NULL, *lru_last = NULL; + bool found = false; + + GIT_ASSERT_ARG(mwf); + GIT_ASSERT_ARG(out_window); + + lru_window = *out_window; + if (out_last) + lru_last = *out_last; + + for (w_last = NULL, w = mwf->windows; w; w_last = w, w = w->next) { + if (w->inuse_cnt) { + if (only_unused) + return false; + /* This window is currently being used. Skip it. */ + continue; + } + + /* + * If the current one is more (or less) recent than the last one, + * store it in the output parameter. If lru_window is NULL, + * it's the first loop, so store it as well. + */ + if (!lru_window || (comparison_sign * w->last_used) > lru_window->last_used) { + lru_window = w; + lru_last = w_last; + found = true; + } + } + + if (!found) + return false; + + *out_window = lru_window; + if (out_last) + *out_last = lru_last; + return true; +} + +/* + * Close the least recently used window (that is currently not being used) out + * of all the files. Called under lock from new_window_locked. + */ +static int git_mwindow_close_lru_window_locked(void) +{ + git_mwindow_ctl *ctl = &git_mwindow__mem_ctl; + git_mwindow_file *cur; + size_t i; + git_mwindow *lru_window = NULL, *lru_last = NULL, **list = NULL; + + git_vector_foreach(&ctl->windowfiles, i, cur) { + if (git_mwindow_scan_recently_used( + cur, &lru_window, &lru_last, false, GIT_MWINDOW__LRU)) { + list = &cur->windows; + } + } + + if (!lru_window) { + git_error_set(GIT_ERROR_OS, "failed to close memory window; couldn't find LRU"); + return -1; + } + + ctl->mapped -= lru_window->window_map.len; + git_futils_mmap_free(&lru_window->window_map); + + if (lru_last) + lru_last->next = lru_window->next; + else + *list = lru_window->next; + + git__free(lru_window); + ctl->open_windows--; + + return 0; +} + +/* + * Finds the file that does not have any open windows AND whose + * most-recently-used window is the least-recently used one across all + * currently open files. + * + * Called under lock from new_window_locked. + */ +static int git_mwindow_find_lru_file_locked(git_mwindow_file **out) +{ + git_mwindow_ctl *ctl = &git_mwindow__mem_ctl; + git_mwindow_file *lru_file = NULL, *current_file = NULL; + git_mwindow *lru_window = NULL; + size_t i; + + git_vector_foreach(&ctl->windowfiles, i, current_file) { + git_mwindow *mru_window = NULL; + if (!git_mwindow_scan_recently_used( + current_file, &mru_window, NULL, true, GIT_MWINDOW__MRU)) { + continue; + } + if (!lru_window || lru_window->last_used > mru_window->last_used) { + lru_window = mru_window; + lru_file = current_file; + } + } + + if (!lru_file) { + git_error_set(GIT_ERROR_OS, "failed to close memory window file; couldn't find LRU"); + return -1; + } + + *out = lru_file; + return 0; +} + +/* This gets called under lock from git_mwindow_open */ +static git_mwindow *new_window_locked( + git_file fd, + off64_t size, + off64_t offset) +{ + git_mwindow_ctl *ctl = &git_mwindow__mem_ctl; + size_t walign = git_mwindow__window_size / 2; + off64_t len; + git_mwindow *w; + + w = git__calloc(1, sizeof(*w)); + + if (w == NULL) + return NULL; + + w->offset = (offset / walign) * walign; + + len = size - w->offset; + if (len > (off64_t)git_mwindow__window_size) + len = (off64_t)git_mwindow__window_size; + + ctl->mapped += (size_t)len; + + while (git_mwindow__mapped_limit < ctl->mapped && + git_mwindow_close_lru_window_locked() == 0) /* nop */; + + /* + * We treat `mapped_limit` as a soft limit. If we can't find a + * window to close and are above the limit, we still mmap the new + * window. + */ + + if (git_futils_mmap_ro(&w->window_map, fd, w->offset, (size_t)len) < 0) { + /* + * The first error might be down to memory fragmentation even if + * we're below our soft limits, so free up what we can and try again. + */ + + while (git_mwindow_close_lru_window_locked() == 0) + /* nop */; + + if (git_futils_mmap_ro(&w->window_map, fd, w->offset, (size_t)len) < 0) { + git__free(w); + return NULL; + } + } + + ctl->mmap_calls++; + ctl->open_windows++; + + if (ctl->mapped > ctl->peak_mapped) + ctl->peak_mapped = ctl->mapped; + + if (ctl->open_windows > ctl->peak_open_windows) + ctl->peak_open_windows = ctl->open_windows; + + return w; +} + +/* + * Open a new window, closing the least recenty used until we have + * enough space. Don't forget to add it to your list + */ +unsigned char *git_mwindow_open( + git_mwindow_file *mwf, + git_mwindow **cursor, + off64_t offset, + size_t extra, + unsigned int *left) +{ + git_mwindow_ctl *ctl = &git_mwindow__mem_ctl; + git_mwindow *w = *cursor; + + if (git_mutex_lock(&git__mwindow_mutex)) { + git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex"); + return NULL; + } + + if (!w || !(git_mwindow_contains(w, offset, extra))) { + if (w) { + w->inuse_cnt--; + } + + for (w = mwf->windows; w; w = w->next) { + if (git_mwindow_contains(w, offset, extra)) + break; + } + + /* + * If there isn't a suitable window, we need to create a new + * one. + */ + if (!w) { + w = new_window_locked(mwf->fd, mwf->size, offset); + if (w == NULL) { + git_mutex_unlock(&git__mwindow_mutex); + return NULL; + } + w->next = mwf->windows; + mwf->windows = w; + } + } + + /* If we changed w, store it in the cursor */ + if (w != *cursor) { + w->last_used = ctl->used_ctr++; + w->inuse_cnt++; + *cursor = w; + } + + offset -= w->offset; + + if (left) + *left = (unsigned int)(w->window_map.len - offset); + + git_mutex_unlock(&git__mwindow_mutex); + return (unsigned char *) w->window_map.data + offset; +} + +int git_mwindow_file_register(git_mwindow_file *mwf) +{ + git_vector closed_files = GIT_VECTOR_INIT; + git_mwindow_ctl *ctl = &git_mwindow__mem_ctl; + int error; + size_t i; + git_mwindow_file *closed_file = NULL; + + if (git_mutex_lock(&git__mwindow_mutex)) { + git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex"); + return -1; + } + + if (ctl->windowfiles.length == 0 && + (error = git_vector_init(&ctl->windowfiles, 8, NULL)) < 0) { + git_mutex_unlock(&git__mwindow_mutex); + goto cleanup; + } + + if (git_mwindow__file_limit) { + git_mwindow_file *lru_file; + while (git_mwindow__file_limit <= ctl->windowfiles.length && + git_mwindow_find_lru_file_locked(&lru_file) == 0) { + if ((error = git_vector_insert(&closed_files, lru_file)) < 0) { + /* + * Exceeding the file limit seems preferable to being open to + * data races that can end up corrupting the heap. + */ + break; + } + git_mwindow_free_all_locked(lru_file); + } + } + + error = git_vector_insert(&ctl->windowfiles, mwf); + git_mutex_unlock(&git__mwindow_mutex); + if (error < 0) + goto cleanup; + + /* + * Once we have released the global windowfiles lock, we can close each + * individual file. Before doing so, acquire that file's lock to avoid + * closing a file that is currently being used. + */ + git_vector_foreach(&closed_files, i, closed_file) { + error = git_mutex_lock(&closed_file->lock); + if (error < 0) + continue; + p_close(closed_file->fd); + closed_file->fd = -1; + git_mutex_unlock(&closed_file->lock); + } + +cleanup: + git_vector_free(&closed_files); + return error; +} + +void git_mwindow_file_deregister(git_mwindow_file *mwf) +{ + git_mwindow_ctl *ctl = &git_mwindow__mem_ctl; + git_mwindow_file *cur; + size_t i; + + if (git_mutex_lock(&git__mwindow_mutex)) + return; + + git_vector_foreach(&ctl->windowfiles, i, cur) { + if (cur == mwf) { + git_vector_remove(&ctl->windowfiles, i); + git_mutex_unlock(&git__mwindow_mutex); + return; + } + } + git_mutex_unlock(&git__mwindow_mutex); +} + +void git_mwindow_close(git_mwindow **window) +{ + git_mwindow *w = *window; + if (w) { + if (git_mutex_lock(&git__mwindow_mutex)) { + git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex"); + return; + } + + w->inuse_cnt--; + git_mutex_unlock(&git__mwindow_mutex); + *window = NULL; + } +} diff --git a/src/libgit2/mwindow.h b/src/libgit2/mwindow.h new file mode 100644 index 0000000..8e6df26 --- /dev/null +++ b/src/libgit2/mwindow.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_mwindow__ +#define INCLUDE_mwindow__ + +#include "common.h" + +#include "map.h" +#include "vector.h" + +typedef struct git_mwindow { + struct git_mwindow *next; + git_map window_map; + off64_t offset; + size_t last_used; + size_t inuse_cnt; +} git_mwindow; + +typedef struct git_mwindow_file { + git_mutex lock; /* protects updates to fd */ + git_mwindow *windows; + int fd; + off64_t size; +} git_mwindow_file; + +typedef struct git_mwindow_ctl { + size_t mapped; + unsigned int open_windows; + unsigned int mmap_calls; + unsigned int peak_open_windows; + size_t peak_mapped; + size_t used_ctr; + git_vector windowfiles; +} git_mwindow_ctl; + +int git_mwindow_contains(git_mwindow *win, off64_t offset, off64_t extra); +int git_mwindow_free_all(git_mwindow_file *mwf); /* locks */ +unsigned char *git_mwindow_open(git_mwindow_file *mwf, git_mwindow **cursor, off64_t offset, size_t extra, unsigned int *left); +int git_mwindow_file_register(git_mwindow_file *mwf); +void git_mwindow_file_deregister(git_mwindow_file *mwf); +void git_mwindow_close(git_mwindow **w_cursor); + +extern int git_mwindow_global_init(void); + +struct git_pack_file; /* just declaration to avoid cyclical includes */ +int git_mwindow_get_pack( + struct git_pack_file **out, + const char *path, + git_oid_t oid_type); +int git_mwindow_put_pack(struct git_pack_file *pack); + +#endif diff --git a/src/libgit2/notes.c b/src/libgit2/notes.c new file mode 100644 index 0000000..13ca382 --- /dev/null +++ b/src/libgit2/notes.c @@ -0,0 +1,810 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "notes.h" + +#include "buf.h" +#include "refs.h" +#include "config.h" +#include "iterator.h" +#include "signature.h" +#include "blob.h" + +static int note_error_notfound(void) +{ + git_error_set(GIT_ERROR_INVALID, "note could not be found"); + return GIT_ENOTFOUND; +} + +static int find_subtree_in_current_level( + git_tree **out, + git_repository *repo, + git_tree *parent, + const char *annotated_object_sha, + int fanout) +{ + size_t i; + const git_tree_entry *entry; + + *out = NULL; + + if (parent == NULL) + return note_error_notfound(); + + for (i = 0; i < git_tree_entrycount(parent); i++) { + entry = git_tree_entry_byindex(parent, i); + + if (!git__ishex(git_tree_entry_name(entry))) + continue; + + if (S_ISDIR(git_tree_entry_filemode(entry)) + && strlen(git_tree_entry_name(entry)) == 2 + && !strncmp(git_tree_entry_name(entry), annotated_object_sha + fanout, 2)) + return git_tree_lookup(out, repo, git_tree_entry_id(entry)); + + /* Not a DIR, so do we have an already existing blob? */ + if (!strcmp(git_tree_entry_name(entry), annotated_object_sha + fanout)) + return GIT_EEXISTS; + } + + return note_error_notfound(); +} + +static int find_subtree_r(git_tree **out, git_tree *root, + git_repository *repo, const char *target, int *fanout) +{ + int error; + git_tree *subtree = NULL; + + *out = NULL; + + error = find_subtree_in_current_level(&subtree, repo, root, target, *fanout); + if (error == GIT_EEXISTS) + return git_tree_lookup(out, repo, git_tree_id(root)); + + if (error < 0) + return error; + + *fanout += 2; + error = find_subtree_r(out, subtree, repo, target, fanout); + git_tree_free(subtree); + + return error; +} + +static int find_blob(git_oid *blob, git_tree *tree, const char *target) +{ + size_t i; + const git_tree_entry *entry; + + for (i=0; iid, note_oid); + + if (git_signature_dup(¬e->author, git_commit_author(commit)) < 0 || + git_signature_dup(¬e->committer, git_commit_committer(commit)) < 0) + return -1; + + blobsize = git_blob_rawsize(blob); + GIT_ERROR_CHECK_BLOBSIZE(blobsize); + + note->message = git__strndup(git_blob_rawcontent(blob), (size_t)blobsize); + GIT_ERROR_CHECK_ALLOC(note->message); + + *out = note; + return 0; +} + +static int note_lookup( + git_note **out, + git_repository *repo, + git_commit *commit, + git_tree *tree, + const char *target) +{ + int error, fanout = 0; + git_oid oid; + git_blob *blob = NULL; + git_note *note = NULL; + git_tree *subtree = NULL; + + if ((error = find_subtree_r(&subtree, tree, repo, target, &fanout)) < 0) + goto cleanup; + + if ((error = find_blob(&oid, subtree, target + fanout)) < 0) + goto cleanup; + + if ((error = git_blob_lookup(&blob, repo, &oid)) < 0) + goto cleanup; + + if ((error = note_new(¬e, &oid, commit, blob)) < 0) + goto cleanup; + + *out = note; + +cleanup: + git_tree_free(subtree); + git_blob_free(blob); + return error; +} + +static int note_remove( + git_oid *notes_commit_out, + git_repository *repo, + const git_signature *author, const git_signature *committer, + const char *notes_ref, git_tree *tree, + const char *target, git_commit **parents) +{ + int error; + git_tree *tree_after_removal = NULL; + git_oid oid; + + if ((error = manipulate_note_in_tree_r( + &tree_after_removal, repo, tree, NULL, target, 0, + remove_note_in_tree_eexists_cb, remove_note_in_tree_enotfound_cb)) < 0) + goto cleanup; + + error = git_commit_create(&oid, repo, notes_ref, author, committer, + NULL, GIT_NOTES_DEFAULT_MSG_RM, + tree_after_removal, + *parents == NULL ? 0 : 1, + (const git_commit **) parents); + + if (error < 0) + goto cleanup; + + if (notes_commit_out) + git_oid_cpy(notes_commit_out, &oid); + +cleanup: + git_tree_free(tree_after_removal); + return error; +} + +static int note_get_default_ref(git_str *out, git_repository *repo) +{ + git_config *cfg; + int error; + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; + + error = git_config__get_string_buf(out, cfg, "core.notesref"); + + if (error == GIT_ENOTFOUND) + error = git_str_puts(out, GIT_NOTES_DEFAULT_REF); + + return error; +} + +static int normalize_namespace(git_str *out, git_repository *repo, const char *notes_ref) +{ + if (notes_ref) + return git_str_puts(out, notes_ref); + + return note_get_default_ref(out, repo); +} + +static int retrieve_note_commit( + git_commit **commit_out, + git_str *notes_ref_out, + git_repository *repo, + const char *notes_ref) +{ + int error; + git_oid oid; + + if ((error = normalize_namespace(notes_ref_out, repo, notes_ref)) < 0) + return error; + + if ((error = git_reference_name_to_id(&oid, repo, notes_ref_out->ptr)) < 0) + return error; + + if (git_commit_lookup(commit_out, repo, &oid) < 0) + return error; + + return 0; +} + +int git_note_commit_read( + git_note **out, + git_repository *repo, + git_commit *notes_commit, + const git_oid *oid) +{ + int error; + git_tree *tree = NULL; + char target[GIT_OID_MAX_HEXSIZE + 1]; + + git_oid_tostr(target, sizeof(target), oid); + + if ((error = git_commit_tree(&tree, notes_commit)) < 0) + goto cleanup; + + error = note_lookup(out, repo, notes_commit, tree, target); + +cleanup: + git_tree_free(tree); + return error; +} + +int git_note_read(git_note **out, git_repository *repo, + const char *notes_ref_in, const git_oid *oid) +{ + int error; + git_str notes_ref = GIT_STR_INIT; + git_commit *commit = NULL; + + error = retrieve_note_commit(&commit, ¬es_ref, repo, notes_ref_in); + + if (error < 0) + goto cleanup; + + error = git_note_commit_read(out, repo, commit, oid); + +cleanup: + git_str_dispose(¬es_ref); + git_commit_free(commit); + return error; +} + +int git_note_commit_create( + git_oid *notes_commit_out, + git_oid *notes_blob_out, + git_repository *repo, + git_commit *parent, + const git_signature *author, + const git_signature *committer, + const git_oid *oid, + const char *note, + int allow_note_overwrite) +{ + int error; + git_tree *tree = NULL; + char target[GIT_OID_MAX_HEXSIZE + 1]; + + git_oid_tostr(target, sizeof(target), oid); + + if (parent != NULL && (error = git_commit_tree(&tree, parent)) < 0) + goto cleanup; + + error = note_write(notes_commit_out, notes_blob_out, repo, author, + committer, NULL, note, tree, target, &parent, allow_note_overwrite); + + if (error < 0) + goto cleanup; + +cleanup: + git_tree_free(tree); + return error; +} + +int git_note_create( + git_oid *out, + git_repository *repo, + const char *notes_ref_in, + const git_signature *author, + const git_signature *committer, + const git_oid *oid, + const char *note, + int allow_note_overwrite) +{ + int error; + git_str notes_ref = GIT_STR_INIT; + git_commit *existing_notes_commit = NULL; + git_reference *ref = NULL; + git_oid notes_blob_oid, notes_commit_oid; + + error = retrieve_note_commit(&existing_notes_commit, ¬es_ref, + repo, notes_ref_in); + + if (error < 0 && error != GIT_ENOTFOUND) + goto cleanup; + + error = git_note_commit_create(¬es_commit_oid, + ¬es_blob_oid, + repo, existing_notes_commit, author, + committer, oid, note, + allow_note_overwrite); + if (error < 0) + goto cleanup; + + error = git_reference_create(&ref, repo, notes_ref.ptr, + ¬es_commit_oid, 1, NULL); + + if (out != NULL) + git_oid_cpy(out, ¬es_blob_oid); + +cleanup: + git_str_dispose(¬es_ref); + git_commit_free(existing_notes_commit); + git_reference_free(ref); + return error; +} + +int git_note_commit_remove( + git_oid *notes_commit_out, + git_repository *repo, + git_commit *notes_commit, + const git_signature *author, + const git_signature *committer, + const git_oid *oid) +{ + int error; + git_tree *tree = NULL; + char target[GIT_OID_MAX_HEXSIZE + 1]; + + git_oid_tostr(target, sizeof(target), oid); + + if ((error = git_commit_tree(&tree, notes_commit)) < 0) + goto cleanup; + + error = note_remove(notes_commit_out, + repo, author, committer, NULL, tree, target, ¬es_commit); + +cleanup: + git_tree_free(tree); + return error; +} + +int git_note_remove(git_repository *repo, const char *notes_ref_in, + const git_signature *author, const git_signature *committer, + const git_oid *oid) +{ + int error; + git_str notes_ref_target = GIT_STR_INIT; + git_commit *existing_notes_commit = NULL; + git_oid new_notes_commit; + git_reference *notes_ref = NULL; + + error = retrieve_note_commit(&existing_notes_commit, ¬es_ref_target, + repo, notes_ref_in); + + if (error < 0) + goto cleanup; + + error = git_note_commit_remove(&new_notes_commit, repo, + existing_notes_commit, author, committer, oid); + if (error < 0) + goto cleanup; + + error = git_reference_create(¬es_ref, repo, notes_ref_target.ptr, + &new_notes_commit, 1, NULL); + +cleanup: + git_str_dispose(¬es_ref_target); + git_reference_free(notes_ref); + git_commit_free(existing_notes_commit); + return error; +} + +int git_note_default_ref(git_buf *out, git_repository *repo) +{ + GIT_BUF_WRAP_PRIVATE(out, note_get_default_ref, repo); +} + +const git_signature *git_note_committer(const git_note *note) +{ + GIT_ASSERT_ARG_WITH_RETVAL(note, NULL); + return note->committer; +} + +const git_signature *git_note_author(const git_note *note) +{ + GIT_ASSERT_ARG_WITH_RETVAL(note, NULL); + return note->author; +} + +const char *git_note_message(const git_note *note) +{ + GIT_ASSERT_ARG_WITH_RETVAL(note, NULL); + return note->message; +} + +const git_oid *git_note_id(const git_note *note) +{ + GIT_ASSERT_ARG_WITH_RETVAL(note, NULL); + return ¬e->id; +} + +void git_note_free(git_note *note) +{ + if (note == NULL) + return; + + git_signature_free(note->committer); + git_signature_free(note->author); + git__free(note->message); + git__free(note); +} + +static int process_entry_path( + git_oid *annotated_object_id, + git_note_iterator *it, + const char *entry_path) +{ + int error = 0; + size_t i = 0, j = 0, len; + git_str buf = GIT_STR_INIT; + + if ((error = git_str_puts(&buf, entry_path)) < 0) + goto cleanup; + + len = git_str_len(&buf); + + while (i < len) { + if (buf.ptr[i] == '/') { + i++; + continue; + } + + if (git__fromhex(buf.ptr[i]) < 0) { + /* This is not a note entry */ + goto cleanup; + } + + if (i != j) + buf.ptr[j] = buf.ptr[i]; + + i++; + j++; + } + + buf.ptr[j] = '\0'; + buf.size = j; + + if (j != git_oid_hexsize(it->repo->oid_type)) { + /* This is not a note entry */ + goto cleanup; + } + + error = git_oid__fromstr(annotated_object_id, buf.ptr, it->repo->oid_type); + +cleanup: + git_str_dispose(&buf); + return error; +} + +int git_note_foreach( + git_repository *repo, + const char *notes_ref, + git_note_foreach_cb note_cb, + void *payload) +{ + int error; + git_note_iterator *iter = NULL; + git_oid note_id, annotated_id; + + if ((error = git_note_iterator_new(&iter, repo, notes_ref)) < 0) + return error; + + while (!(error = git_note_next(¬e_id, &annotated_id, iter))) { + if ((error = note_cb(¬e_id, &annotated_id, payload)) != 0) { + git_error_set_after_callback(error); + break; + } + } + + if (error == GIT_ITEROVER) + error = 0; + + git_note_iterator_free(iter); + return error; +} + +void git_note_iterator_free(git_note_iterator *it) +{ + if (it == NULL) + return; + + git_iterator_free(it); +} + +int git_note_commit_iterator_new( + git_note_iterator **it, + git_commit *notes_commit) +{ + int error; + git_tree *tree; + + if ((error = git_commit_tree(&tree, notes_commit)) < 0) + goto cleanup; + + if ((error = git_iterator_for_tree(it, tree, NULL)) < 0) + git_iterator_free(*it); + +cleanup: + git_tree_free(tree); + + return error; +} + +int git_note_iterator_new( + git_note_iterator **it, + git_repository *repo, + const char *notes_ref_in) +{ + int error; + git_commit *commit = NULL; + git_str notes_ref = GIT_STR_INIT; + + error = retrieve_note_commit(&commit, ¬es_ref, repo, notes_ref_in); + if (error < 0) + goto cleanup; + + error = git_note_commit_iterator_new(it, commit); + +cleanup: + git_str_dispose(¬es_ref); + git_commit_free(commit); + + return error; +} + +int git_note_next( + git_oid *note_id, + git_oid *annotated_id, + git_note_iterator *it) +{ + int error; + const git_index_entry *item; + + if ((error = git_iterator_current(&item, it)) < 0) + return error; + + git_oid_cpy(note_id, &item->id); + + if ((error = process_entry_path(annotated_id, it, item->path)) < 0) + return error; + + if ((error = git_iterator_advance(NULL, it)) < 0 && error != GIT_ITEROVER) + return error; + + return 0; +} diff --git a/src/libgit2/notes.h b/src/libgit2/notes.h new file mode 100644 index 0000000..2168e45 --- /dev/null +++ b/src/libgit2/notes.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_note_h__ +#define INCLUDE_note_h__ + +#include "common.h" + +#include "git2/oid.h" +#include "git2/types.h" + +#define GIT_NOTES_DEFAULT_REF "refs/notes/commits" + +#define GIT_NOTES_DEFAULT_MSG_ADD \ + "Notes added by 'git_note_create' from libgit2" + +#define GIT_NOTES_DEFAULT_MSG_RM \ + "Notes removed by 'git_note_remove' from libgit2" + +struct git_note { + git_oid id; + + git_signature *author; + git_signature *committer; + + char *message; +}; + +#endif diff --git a/src/libgit2/object.c b/src/libgit2/object.c new file mode 100644 index 0000000..5fab77e --- /dev/null +++ b/src/libgit2/object.c @@ -0,0 +1,691 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "object.h" + +#include "git2/object.h" + +#include "repository.h" + +#include "buf.h" +#include "commit.h" +#include "hash.h" +#include "tree.h" +#include "blob.h" +#include "oid.h" +#include "tag.h" + +bool git_object__strict_input_validation = true; + +size_t git_object__size(git_object_t type); + +typedef struct { + const char *str; /* type name string */ + size_t size; /* size in bytes of the object structure */ + + int (*parse)(void *self, git_odb_object *obj, git_oid_t oid_type); + int (*parse_raw)(void *self, const char *data, size_t size, git_oid_t oid_type); + void (*free)(void *self); +} git_object_def; + +static git_object_def git_objects_table[] = { + /* 0 = GIT_OBJECT__EXT1 */ + { "", 0, NULL, NULL, NULL }, + + /* 1 = GIT_OBJECT_COMMIT */ + { "commit", sizeof(git_commit), git_commit__parse, git_commit__parse_raw, git_commit__free }, + + /* 2 = GIT_OBJECT_TREE */ + { "tree", sizeof(git_tree), git_tree__parse, git_tree__parse_raw, git_tree__free }, + + /* 3 = GIT_OBJECT_BLOB */ + { "blob", sizeof(git_blob), git_blob__parse, git_blob__parse_raw, git_blob__free }, + + /* 4 = GIT_OBJECT_TAG */ + { "tag", sizeof(git_tag), git_tag__parse, git_tag__parse_raw, git_tag__free }, + + /* 5 = GIT_OBJECT__EXT2 */ + { "", 0, NULL, NULL, NULL }, + /* 6 = GIT_OBJECT_OFS_DELTA */ + { "OFS_DELTA", 0, NULL, NULL, NULL }, + /* 7 = GIT_OBJECT_REF_DELTA */ + { "REF_DELTA", 0, NULL, NULL, NULL }, +}; + +int git_object__from_raw( + git_object **object_out, + const char *data, + size_t size, + git_object_t object_type, + git_oid_t oid_type) +{ + git_object_def *def; + git_object *object; + size_t object_size; + int error; + + GIT_ASSERT_ARG(object_out); + *object_out = NULL; + + /* Validate type match */ + if (object_type != GIT_OBJECT_BLOB && + object_type != GIT_OBJECT_TREE && + object_type != GIT_OBJECT_COMMIT && + object_type != GIT_OBJECT_TAG) { + git_error_set(GIT_ERROR_INVALID, "the requested type is invalid"); + return GIT_ENOTFOUND; + } + + if ((object_size = git_object__size(object_type)) == 0) { + git_error_set(GIT_ERROR_INVALID, "the requested type is invalid"); + return GIT_ENOTFOUND; + } + + /* Allocate and initialize base object */ + object = git__calloc(1, object_size); + GIT_ERROR_CHECK_ALLOC(object); + object->cached.flags = GIT_CACHE_STORE_PARSED; + object->cached.type = object_type; + if ((error = git_odb__hash(&object->cached.oid, data, size, object_type, oid_type)) < 0) + return error; + + /* Parse raw object data */ + def = &git_objects_table[object_type]; + GIT_ASSERT(def->free && def->parse_raw); + + if ((error = def->parse_raw(object, data, size, oid_type)) < 0) { + def->free(object); + return error; + } + + git_cached_obj_incref(object); + *object_out = object; + + return 0; +} + +int git_object__init_from_odb_object( + git_object **object_out, + git_repository *repo, + git_odb_object *odb_obj, + git_object_t type) +{ + size_t object_size; + git_object *object = NULL; + + GIT_ASSERT_ARG(object_out); + *object_out = NULL; + + /* Validate type match */ + if (type != GIT_OBJECT_ANY && type != odb_obj->cached.type) { + git_error_set(GIT_ERROR_INVALID, + "the requested type does not match the type in the ODB"); + return GIT_ENOTFOUND; + } + + if ((object_size = git_object__size(odb_obj->cached.type)) == 0) { + git_error_set(GIT_ERROR_INVALID, "the requested type is invalid"); + return GIT_ENOTFOUND; + } + + /* Allocate and initialize base object */ + object = git__calloc(1, object_size); + GIT_ERROR_CHECK_ALLOC(object); + + git_oid_cpy(&object->cached.oid, &odb_obj->cached.oid); + object->cached.type = odb_obj->cached.type; + object->cached.size = odb_obj->cached.size; + object->repo = repo; + + *object_out = object; + return 0; +} + +int git_object__from_odb_object( + git_object **object_out, + git_repository *repo, + git_odb_object *odb_obj, + git_object_t type) +{ + int error; + git_object_def *def; + git_object *object = NULL; + + if ((error = git_object__init_from_odb_object(&object, repo, odb_obj, type)) < 0) + return error; + + /* Parse raw object data */ + def = &git_objects_table[odb_obj->cached.type]; + GIT_ASSERT(def->free && def->parse); + + if ((error = def->parse(object, odb_obj, repo->oid_type)) < 0) { + /* + * parse returns EINVALID on invalid data; downgrade + * that to a normal -1 error code. + */ + def->free(object); + return -1; + } + + *object_out = git_cache_store_parsed(&repo->objects, object); + return 0; +} + +void git_object__free(void *obj) +{ + git_object_t type = ((git_object *)obj)->cached.type; + + if (type < 0 || ((size_t)type) >= ARRAY_SIZE(git_objects_table) || + !git_objects_table[type].free) + git__free(obj); + else + git_objects_table[type].free(obj); +} + +int git_object_lookup_prefix( + git_object **object_out, + git_repository *repo, + const git_oid *id, + size_t len, + git_object_t type) +{ + git_object *object = NULL; + git_odb *odb = NULL; + git_odb_object *odb_obj = NULL; + size_t oid_hexsize; + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(object_out); + GIT_ASSERT_ARG(id); + + if (len < GIT_OID_MINPREFIXLEN) { + git_error_set(GIT_ERROR_OBJECT, "ambiguous lookup - OID prefix is too short"); + return GIT_EAMBIGUOUS; + } + + error = git_repository_odb__weakptr(&odb, repo); + if (error < 0) + return error; + + oid_hexsize = git_oid_hexsize(repo->oid_type); + + if (len > oid_hexsize) + len = oid_hexsize; + + if (len == oid_hexsize) { + git_cached_obj *cached = NULL; + + /* We want to match the full id : we can first look up in the cache, + * since there is no need to check for non ambiguousity + */ + cached = git_cache_get_any(&repo->objects, id); + if (cached != NULL) { + if (cached->flags == GIT_CACHE_STORE_PARSED) { + object = (git_object *)cached; + + if (type != GIT_OBJECT_ANY && type != object->cached.type) { + git_object_free(object); + git_error_set(GIT_ERROR_INVALID, + "the requested type does not match the type in the ODB"); + return GIT_ENOTFOUND; + } + + *object_out = object; + return 0; + } else if (cached->flags == GIT_CACHE_STORE_RAW) { + odb_obj = (git_odb_object *)cached; + } else { + GIT_ASSERT(!"Wrong caching type in the global object cache"); + } + } else { + /* Object was not found in the cache, let's explore the backends. + * We could just use git_odb_read_unique_short_oid, + * it is the same cost for packed and loose object backends, + * but it may be much more costly for sqlite and hiredis. + */ + error = git_odb_read(&odb_obj, odb, id); + } + } else { + git_oid short_oid; + + git_oid_clear(&short_oid, repo->oid_type); + git_oid__cpy_prefix(&short_oid, id, len); + + /* If len < GIT_OID_SHA1_HEXSIZE (a strict short oid was given), we have + * 2 options : + * - We always search in the cache first. If we find that short oid is + * ambiguous, we can stop. But in all the other cases, we must then + * explore all the backends (to find an object if there was match, + * or to check that oid is not ambiguous if we have found 1 match in + * the cache) + * - We never explore the cache, go right to exploring the backends + * We chose the latter : we explore directly the backends. + */ + error = git_odb_read_prefix(&odb_obj, odb, &short_oid, len); + } + + if (error < 0) + return error; + + GIT_ASSERT(odb_obj); + error = git_object__from_odb_object(object_out, repo, odb_obj, type); + + git_odb_object_free(odb_obj); + + return error; +} + +int git_object_lookup(git_object **object_out, git_repository *repo, const git_oid *id, git_object_t type) { + return git_object_lookup_prefix(object_out, + repo, id, git_oid_hexsize(repo->oid_type), type); +} + +void git_object_free(git_object *object) +{ + if (object == NULL) + return; + + git_cached_obj_decref(object); +} + +const git_oid *git_object_id(const git_object *obj) +{ + GIT_ASSERT_ARG_WITH_RETVAL(obj, NULL); + return &obj->cached.oid; +} + +git_object_t git_object_type(const git_object *obj) +{ + GIT_ASSERT_ARG_WITH_RETVAL(obj, GIT_OBJECT_INVALID); + return obj->cached.type; +} + +git_repository *git_object_owner(const git_object *obj) +{ + GIT_ASSERT_ARG_WITH_RETVAL(obj, NULL); + return obj->repo; +} + +const char *git_object_type2string(git_object_t type) +{ + if (type < 0 || ((size_t) type) >= ARRAY_SIZE(git_objects_table)) + return ""; + + return git_objects_table[type].str; +} + +git_object_t git_object_string2type(const char *str) +{ + if (!str) + return GIT_OBJECT_INVALID; + + return git_object_stringn2type(str, strlen(str)); +} + +git_object_t git_object_stringn2type(const char *str, size_t len) +{ + size_t i; + + if (!str || !len || !*str) + return GIT_OBJECT_INVALID; + + for (i = 0; i < ARRAY_SIZE(git_objects_table); i++) + if (*git_objects_table[i].str && + !git__prefixncmp(str, len, git_objects_table[i].str)) + return (git_object_t)i; + + return GIT_OBJECT_INVALID; +} + +int git_object_typeisloose(git_object_t type) +{ + if (type < 0 || ((size_t) type) >= ARRAY_SIZE(git_objects_table)) + return 0; + + return (git_objects_table[type].size > 0) ? 1 : 0; +} + +size_t git_object__size(git_object_t type) +{ + if (type < 0 || ((size_t) type) >= ARRAY_SIZE(git_objects_table)) + return 0; + + return git_objects_table[type].size; +} + +static int dereference_object(git_object **dereferenced, git_object *obj) +{ + git_object_t type = git_object_type(obj); + + switch (type) { + case GIT_OBJECT_COMMIT: + return git_commit_tree((git_tree **)dereferenced, (git_commit*)obj); + + case GIT_OBJECT_TAG: + return git_tag_target(dereferenced, (git_tag*)obj); + + case GIT_OBJECT_BLOB: + case GIT_OBJECT_TREE: + return GIT_EPEEL; + + default: + return GIT_EINVALIDSPEC; + } +} + +static int peel_error(int error, const git_oid *oid, git_object_t type) +{ + const char *type_name; + char hex_oid[GIT_OID_MAX_HEXSIZE + 1]; + + type_name = git_object_type2string(type); + + git_oid_nfmt(hex_oid, GIT_OID_MAX_HEXSIZE + 1, oid); + + git_error_set(GIT_ERROR_OBJECT, "the git_object of id '%s' can not be " + "successfully peeled into a %s (git_object_t=%i).", hex_oid, type_name, type); + + return error; +} + +static int check_type_combination(git_object_t type, git_object_t target) +{ + if (type == target) + return 0; + + switch (type) { + case GIT_OBJECT_BLOB: + case GIT_OBJECT_TREE: + /* a blob or tree can never be peeled to anything but themselves */ + return GIT_EINVALIDSPEC; + break; + case GIT_OBJECT_COMMIT: + /* a commit can only be peeled to a tree */ + if (target != GIT_OBJECT_TREE && target != GIT_OBJECT_ANY) + return GIT_EINVALIDSPEC; + break; + case GIT_OBJECT_TAG: + /* a tag may point to anything, so we let anything through */ + break; + default: + return GIT_EINVALIDSPEC; + } + + return 0; +} + +int git_object_peel( + git_object **peeled, + const git_object *object, + git_object_t target_type) +{ + git_object *source, *deref = NULL; + int error; + + GIT_ASSERT_ARG(object); + GIT_ASSERT_ARG(peeled); + + GIT_ASSERT_ARG(target_type == GIT_OBJECT_TAG || + target_type == GIT_OBJECT_COMMIT || + target_type == GIT_OBJECT_TREE || + target_type == GIT_OBJECT_BLOB || + target_type == GIT_OBJECT_ANY); + + if ((error = check_type_combination(git_object_type(object), target_type)) < 0) + return peel_error(error, git_object_id(object), target_type); + + if (git_object_type(object) == target_type) + return git_object_dup(peeled, (git_object *)object); + + source = (git_object *)object; + + while (!(error = dereference_object(&deref, source))) { + + if (source != object) + git_object_free(source); + + if (git_object_type(deref) == target_type) { + *peeled = deref; + return 0; + } + + if (target_type == GIT_OBJECT_ANY && + git_object_type(deref) != git_object_type(object)) + { + *peeled = deref; + return 0; + } + + source = deref; + deref = NULL; + } + + if (source != object) + git_object_free(source); + + git_object_free(deref); + + if (error) + error = peel_error(error, git_object_id(object), target_type); + + return error; +} + +int git_object_dup(git_object **dest, git_object *source) +{ + git_cached_obj_incref(source); + *dest = source; + return 0; +} + +int git_object_lookup_bypath( + git_object **out, + const git_object *treeish, + const char *path, + git_object_t type) +{ + int error = -1; + git_tree *tree = NULL; + git_tree_entry *entry = NULL; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(treeish); + GIT_ASSERT_ARG(path); + + if ((error = git_object_peel((git_object**)&tree, treeish, GIT_OBJECT_TREE)) < 0 || + (error = git_tree_entry_bypath(&entry, tree, path)) < 0) + { + goto cleanup; + } + + if (type != GIT_OBJECT_ANY && git_tree_entry_type(entry) != type) + { + git_error_set(GIT_ERROR_OBJECT, + "object at path '%s' is not of the asked-for type %d", + path, type); + error = GIT_EINVALIDSPEC; + goto cleanup; + } + + error = git_tree_entry_to_object(out, git_object_owner(treeish), entry); + +cleanup: + git_tree_entry_free(entry); + git_tree_free(tree); + return error; +} + +static int git_object__short_id(git_str *out, const git_object *obj) +{ + git_repository *repo; + git_oid id; + git_odb *odb; + size_t oid_hexsize; + int len = GIT_ABBREV_DEFAULT, error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(obj); + + repo = git_object_owner(obj); + + git_oid_clear(&id, repo->oid_type); + oid_hexsize = git_oid_hexsize(repo->oid_type); + + if ((error = git_repository__configmap_lookup(&len, repo, GIT_CONFIGMAP_ABBREV)) < 0) + return error; + + if (len < 0 || (size_t)len > oid_hexsize) { + git_error_set(GIT_ERROR_CONFIG, "invalid oid abbreviation setting: '%d'", len); + return -1; + } + + if ((error = git_repository_odb(&odb, repo)) < 0) + return error; + + while ((size_t)len < oid_hexsize) { + /* set up short oid */ + memcpy(&id.id, &obj->cached.oid.id, (len + 1) / 2); + if (len & 1) + id.id[len / 2] &= 0xf0; + + error = git_odb_exists_prefix(NULL, odb, &id, len); + if (error != GIT_EAMBIGUOUS) + break; + + git_error_clear(); + len++; + } + + if (!error && !(error = git_str_grow(out, len + 1))) { + git_oid_tostr(out->ptr, len + 1, &id); + out->size = len; + } + + git_odb_free(odb); + + return error; +} + +int git_object_short_id(git_buf *out, const git_object *obj) +{ + GIT_BUF_WRAP_PRIVATE(out, git_object__short_id, obj); +} + +bool git_object__is_valid( + git_repository *repo, const git_oid *id, git_object_t expected_type) +{ + git_odb *odb; + git_object_t actual_type; + size_t len; + int error; + + if (!git_object__strict_input_validation) + return true; + + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0 || + (error = git_odb_read_header(&len, &actual_type, odb, id)) < 0) + return false; + + if (expected_type != GIT_OBJECT_ANY && expected_type != actual_type) { + git_error_set(GIT_ERROR_INVALID, + "the requested type does not match the type in the ODB"); + return false; + } + + return true; +} + +int git_object_rawcontent_is_valid( + int *valid, + const char *buf, + size_t len, + git_object_t object_type +#ifdef GIT_EXPERIMENTAL_SHA256 + , git_oid_t oid_type +#endif + ) +{ + git_object *obj = NULL; + int error; + +#ifndef GIT_EXPERIMENTAL_SHA256 + git_oid_t oid_type = GIT_OID_SHA1; +#endif + + GIT_ASSERT_ARG(valid); + GIT_ASSERT_ARG(buf); + + /* Blobs are always valid; don't bother parsing. */ + if (object_type == GIT_OBJECT_BLOB) { + *valid = 1; + return 0; + } + + error = git_object__from_raw(&obj, buf, len, object_type, oid_type); + git_object_free(obj); + + if (error == 0) { + *valid = 1; + return 0; + } else if (error == GIT_EINVALID) { + *valid = 0; + return 0; + } + + return error; +} + +int git_object__parse_oid_header( + git_oid *oid, + const char **buffer_out, + const char *buffer_end, + const char *header, + git_oid_t oid_type) +{ + const size_t sha_len = git_oid_hexsize(oid_type); + const size_t header_len = strlen(header); + + const char *buffer = *buffer_out; + + if (buffer + (header_len + sha_len + 1) > buffer_end) + return -1; + + if (memcmp(buffer, header, header_len) != 0) + return -1; + + if (buffer[header_len + sha_len] != '\n') + return -1; + + if (git_oid__fromstr(oid, buffer + header_len, oid_type) < 0) + return -1; + + *buffer_out = buffer + (header_len + sha_len + 1); + + return 0; +} + +int git_object__write_oid_header( + git_str *buf, + const char *header, + const git_oid *oid) +{ + size_t hex_size = git_oid_hexsize(git_oid_type(oid)); + char hex_oid[GIT_OID_MAX_HEXSIZE]; + + if (!hex_size) { + git_error_set(GIT_ERROR_INVALID, "unknown type"); + return -1; + } + + git_oid_fmt(hex_oid, oid); + git_str_puts(buf, header); + git_str_put(buf, hex_oid, hex_size); + git_str_putc(buf, '\n'); + + return git_str_oom(buf) ? -1 : 0; +} diff --git a/src/libgit2/object.h b/src/libgit2/object.h new file mode 100644 index 0000000..b6c604c --- /dev/null +++ b/src/libgit2/object.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_object_h__ +#define INCLUDE_object_h__ + +#include "common.h" + +#include "repository.h" + +#define GIT_OBJECT_SIZE_MAX UINT64_MAX + +extern bool git_object__strict_input_validation; + +/** Base git object for inheritance */ +struct git_object { + git_cached_obj cached; + git_repository *repo; +}; + +/* fully free the object; internal method, DO NOT EXPORT */ +void git_object__free(void *object); + +/* + * Parse object from raw data. Note that the resulting object is + * tied to the lifetime of the data, as some objects simply point + * into it. + */ +int git_object__from_raw( + git_object **object_out, + const char *data, + size_t size, + git_object_t object_type, + git_oid_t oid_type); + +int git_object__init_from_odb_object( + git_object **object_out, + git_repository *repo, + git_odb_object *odb_obj, + git_object_t type); + +int git_object__from_odb_object( + git_object **object_out, + git_repository *repo, + git_odb_object *odb_obj, + git_object_t type); + +int git_object__resolve_to_type(git_object **obj, git_object_t type); + +git_object_t git_object_stringn2type(const char *str, size_t len); + +int git_object__parse_oid_header( + git_oid *oid, + const char **buffer_out, + const char *buffer_end, + const char *header, + git_oid_t oid_type); + +int git_object__write_oid_header( + git_str *buf, + const char *header, + const git_oid *oid); + +bool git_object__is_valid( + git_repository *repo, const git_oid *id, git_object_t expected_type); + +GIT_INLINE(git_object_t) git_object__type_from_filemode(git_filemode_t mode) +{ + switch (mode) { + case GIT_FILEMODE_TREE: + return GIT_OBJECT_TREE; + case GIT_FILEMODE_COMMIT: + return GIT_OBJECT_COMMIT; + case GIT_FILEMODE_BLOB: + case GIT_FILEMODE_BLOB_EXECUTABLE: + case GIT_FILEMODE_LINK: + return GIT_OBJECT_BLOB; + default: + return GIT_OBJECT_INVALID; + } +} + +#endif diff --git a/src/libgit2/object_api.c b/src/libgit2/object_api.c new file mode 100644 index 0000000..d45abd5 --- /dev/null +++ b/src/libgit2/object_api.c @@ -0,0 +1,148 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "git2/object.h" + +#include "repository.h" +#include "commit.h" +#include "tree.h" +#include "blob.h" +#include "tag.h" + +/** + * Commit + */ +int git_commit_lookup(git_commit **out, git_repository *repo, const git_oid *id) +{ + return git_object_lookup((git_object **)out, repo, id, GIT_OBJECT_COMMIT); +} + +int git_commit_lookup_prefix(git_commit **out, git_repository *repo, const git_oid *id, size_t len) +{ + return git_object_lookup_prefix((git_object **)out, repo, id, len, GIT_OBJECT_COMMIT); +} + +void git_commit_free(git_commit *obj) +{ + git_object_free((git_object *)obj); +} + +const git_oid *git_commit_id(const git_commit *obj) +{ + return git_object_id((const git_object *)obj); +} + +git_repository *git_commit_owner(const git_commit *obj) +{ + return git_object_owner((const git_object *)obj); +} + +int git_commit_dup(git_commit **out, git_commit *obj) +{ + return git_object_dup((git_object **)out, (git_object *)obj); +} + +/** + * Tree + */ +int git_tree_lookup(git_tree **out, git_repository *repo, const git_oid *id) +{ + return git_object_lookup((git_object **)out, repo, id, GIT_OBJECT_TREE); +} + +int git_tree_lookup_prefix(git_tree **out, git_repository *repo, const git_oid *id, size_t len) +{ + return git_object_lookup_prefix((git_object **)out, repo, id, len, GIT_OBJECT_TREE); +} + +void git_tree_free(git_tree *obj) +{ + git_object_free((git_object *)obj); +} + +const git_oid *git_tree_id(const git_tree *obj) +{ + return git_object_id((const git_object *)obj); +} + +git_repository *git_tree_owner(const git_tree *obj) +{ + return git_object_owner((const git_object *)obj); +} + +int git_tree_dup(git_tree **out, git_tree *obj) +{ + return git_object_dup((git_object **)out, (git_object *)obj); +} + +/** + * Tag + */ +int git_tag_lookup(git_tag **out, git_repository *repo, const git_oid *id) +{ + return git_object_lookup((git_object **)out, repo, id, GIT_OBJECT_TAG); +} + +int git_tag_lookup_prefix(git_tag **out, git_repository *repo, const git_oid *id, size_t len) +{ + return git_object_lookup_prefix((git_object **)out, repo, id, len, GIT_OBJECT_TAG); +} + +void git_tag_free(git_tag *obj) +{ + git_object_free((git_object *)obj); +} + +const git_oid *git_tag_id(const git_tag *obj) +{ + return git_object_id((const git_object *)obj); +} + +git_repository *git_tag_owner(const git_tag *obj) +{ + return git_object_owner((const git_object *)obj); +} + +int git_tag_dup(git_tag **out, git_tag *obj) +{ + return git_object_dup((git_object **)out, (git_object *)obj); +} + +/** + * Blob + */ +int git_blob_lookup(git_blob **out, git_repository *repo, const git_oid *id) +{ + return git_object_lookup((git_object **)out, repo, id, GIT_OBJECT_BLOB); +} + +int git_blob_lookup_prefix(git_blob **out, git_repository *repo, const git_oid *id, size_t len) +{ + return git_object_lookup_prefix((git_object **)out, repo, id, len, GIT_OBJECT_BLOB); +} + +void git_blob_free(git_blob *obj) +{ + git_object_free((git_object *)obj); +} + +const git_oid *git_blob_id(const git_blob *obj) +{ + return git_object_id((const git_object *)obj); +} + +git_repository *git_blob_owner(const git_blob *obj) +{ + return git_object_owner((const git_object *)obj); +} + +int git_blob_dup(git_blob **out, git_blob *obj) +{ + return git_object_dup((git_object **)out, (git_object *)obj); +} diff --git a/src/libgit2/odb.c b/src/libgit2/odb.c new file mode 100644 index 0000000..fec1e45 --- /dev/null +++ b/src/libgit2/odb.c @@ -0,0 +1,2000 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "odb.h" + +#include +#include "git2/object.h" +#include "git2/sys/odb_backend.h" +#include "futils.h" +#include "hash.h" +#include "delta.h" +#include "filter.h" +#include "repository.h" +#include "blob.h" +#include "oid.h" + +#include "git2/odb_backend.h" +#include "git2/oid.h" +#include "git2/oidarray.h" + +#define GIT_ALTERNATES_FILE "info/alternates" + +#define GIT_ALTERNATES_MAX_DEPTH 5 + +/* + * We work under the assumption that most objects for long-running + * operations will be packed + */ +int git_odb__loose_priority = GIT_ODB_DEFAULT_LOOSE_PRIORITY; +int git_odb__packed_priority = GIT_ODB_DEFAULT_PACKED_PRIORITY; + +bool git_odb__strict_hash_verification = true; + +typedef struct +{ + git_odb_backend *backend; + int priority; + bool is_alternate; + ino_t disk_inode; +} backend_internal; + +static git_cache *odb_cache(git_odb *odb) +{ + git_repository *owner = GIT_REFCOUNT_OWNER(odb); + if (owner != NULL) { + return &owner->objects; + } + + return &odb->own_cache; +} + +static int odb_otype_fast(git_object_t *type_p, git_odb *db, const git_oid *id); +static int load_alternates(git_odb *odb, const char *objects_dir, int alternate_depth); +static int error_null_oid(int error, const char *message); + +static git_object_t odb_hardcoded_type(const git_oid *id) +{ + if (!git_oid_cmp(id, &git_oid__empty_tree_sha1)) + return GIT_OBJECT_TREE; + + return GIT_OBJECT_INVALID; +} + +static int odb_read_hardcoded(bool *found, git_rawobj *raw, const git_oid *id) +{ + git_object_t type; + + *found = false; + + if ((type = odb_hardcoded_type(id)) == GIT_OBJECT_INVALID) + return 0; + + raw->type = type; + raw->len = 0; + raw->data = git__calloc(1, sizeof(uint8_t)); + GIT_ERROR_CHECK_ALLOC(raw->data); + + *found = true; + return 0; +} + +int git_odb__format_object_header( + size_t *written, + char *hdr, + size_t hdr_size, + git_object_size_t obj_len, + git_object_t obj_type) +{ + const char *type_str = git_object_type2string(obj_type); + int hdr_max = (hdr_size > INT_MAX-2) ? (INT_MAX-2) : (int)hdr_size; + int len; + + len = p_snprintf(hdr, hdr_max, "%s %"PRId64, type_str, (int64_t)obj_len); + + if (len < 0 || len >= hdr_max) { + git_error_set(GIT_ERROR_OS, "object header creation failed"); + return -1; + } + + *written = (size_t)(len + 1); + return 0; +} + +int git_odb__hashobj(git_oid *id, git_rawobj *obj, git_oid_t oid_type) +{ + git_str_vec vec[2]; + char header[64]; + size_t hdrlen; + git_hash_algorithm_t algorithm; + int error; + + GIT_ASSERT_ARG(id); + GIT_ASSERT_ARG(obj); + + if (!git_object_typeisloose(obj->type)) { + git_error_set(GIT_ERROR_INVALID, "invalid object type"); + return -1; + } + + if (!(algorithm = git_oid_algorithm(oid_type))) { + git_error_set(GIT_ERROR_INVALID, "unknown oid type"); + return -1; + } + + if (!obj->data && obj->len != 0) { + git_error_set(GIT_ERROR_INVALID, "invalid object"); + return -1; + } + + if ((error = git_odb__format_object_header(&hdrlen, + header, sizeof(header), obj->len, obj->type)) < 0) + return error; + + vec[0].data = header; + vec[0].len = hdrlen; + vec[1].data = obj->data; + vec[1].len = obj->len; + +#ifdef GIT_EXPERIMENTAL_SHA256 + id->type = oid_type; +#endif + + return git_hash_vec(id->id, vec, 2, algorithm); +} + + +static git_odb_object *odb_object__alloc(const git_oid *oid, git_rawobj *source) +{ + git_odb_object *object = git__calloc(1, sizeof(git_odb_object)); + + if (object != NULL) { + git_oid_cpy(&object->cached.oid, oid); + object->cached.type = source->type; + object->cached.size = source->len; + object->buffer = source->data; + } + + return object; +} + +void git_odb_object__free(void *object) +{ + if (object != NULL) { + git__free(((git_odb_object *)object)->buffer); + git__free(object); + } +} + +const git_oid *git_odb_object_id(git_odb_object *object) +{ + return &object->cached.oid; +} + +const void *git_odb_object_data(git_odb_object *object) +{ + return object->buffer; +} + +size_t git_odb_object_size(git_odb_object *object) +{ + return object->cached.size; +} + +git_object_t git_odb_object_type(git_odb_object *object) +{ + return object->cached.type; +} + +int git_odb_object_dup(git_odb_object **dest, git_odb_object *source) +{ + git_cached_obj_incref(source); + *dest = source; + return 0; +} + +void git_odb_object_free(git_odb_object *object) +{ + if (object == NULL) + return; + + git_cached_obj_decref(object); +} + +int git_odb__hashfd( + git_oid *out, + git_file fd, + size_t size, + git_object_t object_type, + git_oid_t oid_type) +{ + size_t hdr_len; + char hdr[64], buffer[GIT_BUFSIZE_FILEIO]; + git_hash_ctx ctx; + git_hash_algorithm_t algorithm; + ssize_t read_len = 0; + int error = 0; + + if (!git_object_typeisloose(object_type)) { + git_error_set(GIT_ERROR_INVALID, "invalid object type for hash"); + return -1; + } + + if (!(algorithm = git_oid_algorithm(oid_type))) { + git_error_set(GIT_ERROR_INVALID, "unknown oid type"); + return -1; + } + + if ((error = git_hash_ctx_init(&ctx, algorithm)) < 0) + return error; + + if ((error = git_odb__format_object_header(&hdr_len, hdr, + sizeof(hdr), size, object_type)) < 0) + goto done; + + if ((error = git_hash_update(&ctx, hdr, hdr_len)) < 0) + goto done; + + while (size > 0 && (read_len = p_read(fd, buffer, sizeof(buffer))) > 0) { + if ((error = git_hash_update(&ctx, buffer, read_len)) < 0) + goto done; + + size -= read_len; + } + + /* If p_read returned an error code, the read obviously failed. + * If size is not zero, the file was truncated after we originally + * stat'd it, so we consider this a read failure too */ + if (read_len < 0 || size > 0) { + git_error_set(GIT_ERROR_OS, "error reading file for hashing"); + error = -1; + + goto done; + } + + error = git_hash_final(out->id, &ctx); + +#ifdef GIT_EXPERIMENTAL_SHA256 + out->type = oid_type; +#endif + +done: + git_hash_ctx_cleanup(&ctx); + return error; +} + +int git_odb__hashfd_filtered( + git_oid *out, + git_file fd, + size_t size, + git_object_t object_type, + git_oid_t oid_type, + git_filter_list *fl) +{ + int error; + git_str raw = GIT_STR_INIT; + + if (!fl) + return git_odb__hashfd(out, fd, size, object_type, oid_type); + + /* size of data is used in header, so we have to read the whole file + * into memory to apply filters before beginning to calculate the hash + */ + + if (!(error = git_futils_readbuffer_fd(&raw, fd, size))) { + git_str post = GIT_STR_INIT; + + error = git_filter_list__convert_buf(&post, fl, &raw); + + if (!error) + error = git_odb__hash(out, post.ptr, post.size, object_type, oid_type); + + git_str_dispose(&post); + } + + return error; +} + +int git_odb__hashlink(git_oid *out, const char *path, git_oid_t oid_type) +{ + struct stat st; + int size; + int result; + + if (git_fs_path_lstat(path, &st) < 0) + return -1; + + if (!git__is_int(st.st_size) || (int)st.st_size < 0) { + git_error_set(GIT_ERROR_FILESYSTEM, "file size overflow for 32-bit systems"); + return -1; + } + + size = (int)st.st_size; + + if (S_ISLNK(st.st_mode)) { + char *link_data; + int read_len; + size_t alloc_size; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_size, size, 1); + link_data = git__malloc(alloc_size); + GIT_ERROR_CHECK_ALLOC(link_data); + + read_len = p_readlink(path, link_data, size); + if (read_len == -1) { + git_error_set(GIT_ERROR_OS, "failed to read symlink data for '%s'", path); + git__free(link_data); + return -1; + } + GIT_ASSERT(read_len <= size); + link_data[read_len] = '\0'; + + result = git_odb__hash(out, link_data, read_len, GIT_OBJECT_BLOB, oid_type); + git__free(link_data); + } else { + int fd = git_futils_open_ro(path); + if (fd < 0) + return -1; + result = git_odb__hashfd(out, fd, size, GIT_OBJECT_BLOB, oid_type); + p_close(fd); + } + + return result; +} + +int git_odb__hashfile( + git_oid *out, + const char *path, + git_object_t object_type, + git_oid_t oid_type) +{ + uint64_t size; + int fd, error = 0; + + if ((fd = git_futils_open_ro(path)) < 0) + return fd; + + if ((error = git_futils_filesize(&size, fd)) < 0) + goto done; + + if (!git__is_sizet(size)) { + git_error_set(GIT_ERROR_OS, "file size overflow for 32-bit systems"); + error = -1; + goto done; + } + + error = git_odb__hashfd(out, fd, (size_t)size, object_type, oid_type); + +done: + p_close(fd); + return error; +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_odb_hashfile( + git_oid *out, + const char *path, + git_object_t object_type, + git_oid_t oid_type) +{ + return git_odb__hashfile(out, path, object_type, oid_type); +} +#else +int git_odb_hashfile( + git_oid *out, + const char *path, + git_object_t object_type) +{ + return git_odb__hashfile(out, path, object_type, GIT_OID_SHA1); +} +#endif + +int git_odb__hash( + git_oid *id, + const void *data, + size_t len, + git_object_t object_type, + git_oid_t oid_type) +{ + git_rawobj raw; + + GIT_ASSERT_ARG(id); + + raw.data = (void *)data; + raw.len = len; + raw.type = object_type; + + return git_odb__hashobj(id, &raw, oid_type); +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_odb_hash( + git_oid *out, + const void *data, + size_t len, + git_object_t object_type, + git_oid_t oid_type) +{ + return git_odb__hash(out, data, len, object_type, oid_type); +} +#else +int git_odb_hash( + git_oid *out, + const void *data, + size_t len, + git_object_t type) +{ + return git_odb__hash(out, data, len, type, GIT_OID_SHA1); +} +#endif + +/** + * FAKE WSTREAM + */ + +typedef struct { + git_odb_stream stream; + char *buffer; + size_t size, written; + git_object_t type; +} fake_wstream; + +static int fake_wstream__fwrite(git_odb_stream *_stream, const git_oid *oid) +{ + fake_wstream *stream = (fake_wstream *)_stream; + return _stream->backend->write(_stream->backend, oid, stream->buffer, stream->size, stream->type); +} + +static int fake_wstream__write(git_odb_stream *_stream, const char *data, size_t len) +{ + fake_wstream *stream = (fake_wstream *)_stream; + + GIT_ASSERT(stream->written + len <= stream->size); + + memcpy(stream->buffer + stream->written, data, len); + stream->written += len; + return 0; +} + +static void fake_wstream__free(git_odb_stream *_stream) +{ + fake_wstream *stream = (fake_wstream *)_stream; + + git__free(stream->buffer); + git__free(stream); +} + +static int init_fake_wstream(git_odb_stream **stream_p, git_odb_backend *backend, git_object_size_t size, git_object_t type) +{ + fake_wstream *stream; + size_t blobsize; + + GIT_ERROR_CHECK_BLOBSIZE(size); + blobsize = (size_t)size; + + stream = git__calloc(1, sizeof(fake_wstream)); + GIT_ERROR_CHECK_ALLOC(stream); + + stream->size = blobsize; + stream->type = type; + stream->buffer = git__malloc(blobsize); + if (stream->buffer == NULL) { + git__free(stream); + return -1; + } + + stream->stream.backend = backend; + stream->stream.read = NULL; /* read only */ + stream->stream.write = &fake_wstream__write; + stream->stream.finalize_write = &fake_wstream__fwrite; + stream->stream.free = &fake_wstream__free; + stream->stream.mode = GIT_STREAM_WRONLY; + + *stream_p = (git_odb_stream *)stream; + return 0; +} + +/*********************************************************** + * + * OBJECT DATABASE PUBLIC API + * + * Public calls for the ODB functionality + * + ***********************************************************/ + +static int backend_sort_cmp(const void *a, const void *b) +{ + const backend_internal *backend_a = (const backend_internal *)(a); + const backend_internal *backend_b = (const backend_internal *)(b); + + if (backend_b->priority == backend_a->priority) { + if (backend_a->is_alternate) + return -1; + if (backend_b->is_alternate) + return 1; + return 0; + } + return (backend_b->priority - backend_a->priority); +} + +static void normalize_options( + git_odb_options *opts, + const git_odb_options *given_opts) +{ + git_odb_options init = GIT_ODB_OPTIONS_INIT; + + if (given_opts) + memcpy(opts, given_opts, sizeof(git_odb_options)); + else + memcpy(opts, &init, sizeof(git_odb_options)); + + if (!opts->oid_type) + opts->oid_type = GIT_OID_DEFAULT; +} + +int git_odb__new(git_odb **out, const git_odb_options *opts) +{ + git_odb *db = git__calloc(1, sizeof(*db)); + GIT_ERROR_CHECK_ALLOC(db); + + normalize_options(&db->options, opts); + + if (git_mutex_init(&db->lock) < 0) { + git__free(db); + return -1; + } + if (git_cache_init(&db->own_cache) < 0) { + git_mutex_free(&db->lock); + git__free(db); + return -1; + } + if (git_vector_init(&db->backends, 4, backend_sort_cmp) < 0) { + git_cache_dispose(&db->own_cache); + git_mutex_free(&db->lock); + git__free(db); + return -1; + } + + *out = db; + GIT_REFCOUNT_INC(db); + return 0; +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_odb_new(git_odb **out, const git_odb_options *opts) +{ + return git_odb__new(out, opts); +} +#else +int git_odb_new(git_odb **out) +{ + return git_odb__new(out, NULL); +} +#endif + +static int add_backend_internal( + git_odb *odb, git_odb_backend *backend, + int priority, bool is_alternate, ino_t disk_inode) +{ + backend_internal *internal; + + GIT_ASSERT_ARG(odb); + GIT_ASSERT_ARG(backend); + + GIT_ERROR_CHECK_VERSION(backend, GIT_ODB_BACKEND_VERSION, "git_odb_backend"); + + /* Check if the backend is already owned by another ODB */ + GIT_ASSERT(!backend->odb || backend->odb == odb); + + internal = git__malloc(sizeof(backend_internal)); + GIT_ERROR_CHECK_ALLOC(internal); + + internal->backend = backend; + internal->priority = priority; + internal->is_alternate = is_alternate; + internal->disk_inode = disk_inode; + + if (git_mutex_lock(&odb->lock) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return -1; + } + if (git_vector_insert(&odb->backends, internal) < 0) { + git_mutex_unlock(&odb->lock); + git__free(internal); + return -1; + } + git_vector_sort(&odb->backends); + internal->backend->odb = odb; + git_mutex_unlock(&odb->lock); + return 0; +} + +int git_odb_add_backend(git_odb *odb, git_odb_backend *backend, int priority) +{ + return add_backend_internal(odb, backend, priority, false, 0); +} + +int git_odb_add_alternate(git_odb *odb, git_odb_backend *backend, int priority) +{ + return add_backend_internal(odb, backend, priority, true, 0); +} + +size_t git_odb_num_backends(git_odb *odb) +{ + size_t length; + bool locked = true; + + GIT_ASSERT_ARG(odb); + + if (git_mutex_lock(&odb->lock) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + locked = false; + } + length = odb->backends.length; + if (locked) + git_mutex_unlock(&odb->lock); + return length; +} + +static int git_odb__error_unsupported_in_backend(const char *action) +{ + git_error_set(GIT_ERROR_ODB, + "cannot %s - unsupported in the loaded odb backends", action); + return -1; +} + + +int git_odb_get_backend(git_odb_backend **out, git_odb *odb, size_t pos) +{ + backend_internal *internal; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(odb); + + + if ((error = git_mutex_lock(&odb->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + internal = git_vector_get(&odb->backends, pos); + + if (!internal || !internal->backend) { + git_mutex_unlock(&odb->lock); + + git_error_set(GIT_ERROR_ODB, "no ODB backend loaded at index %" PRIuZ, pos); + return GIT_ENOTFOUND; + } + *out = internal->backend; + git_mutex_unlock(&odb->lock); + + return 0; +} + +int git_odb__add_default_backends( + git_odb *db, const char *objects_dir, + bool as_alternates, int alternate_depth) +{ + size_t i = 0; + struct stat st; + ino_t inode; + git_odb_backend *loose, *packed; + git_odb_backend_loose_options loose_opts = GIT_ODB_BACKEND_LOOSE_OPTIONS_INIT; + git_odb_backend_pack_options pack_opts = GIT_ODB_BACKEND_PACK_OPTIONS_INIT; + + /* TODO: inodes are not really relevant on Win32, so we need to find + * a cross-platform workaround for this */ +#ifdef GIT_WIN32 + GIT_UNUSED(i); + GIT_UNUSED(&st); + + inode = 0; +#else + if (p_stat(objects_dir, &st) < 0) { + if (as_alternates) + /* this should warn */ + return 0; + + git_error_set(GIT_ERROR_ODB, "failed to load object database in '%s'", objects_dir); + return -1; + } + + inode = st.st_ino; + + if (git_mutex_lock(&db->lock) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return -1; + } + for (i = 0; i < db->backends.length; ++i) { + backend_internal *backend = git_vector_get(&db->backends, i); + if (backend->disk_inode == inode) { + git_mutex_unlock(&db->lock); + return 0; + } + } + git_mutex_unlock(&db->lock); +#endif + + if (db->do_fsync) + loose_opts.flags |= GIT_ODB_BACKEND_LOOSE_FSYNC; + + loose_opts.oid_type = db->options.oid_type; + pack_opts.oid_type = db->options.oid_type; + + /* add the loose object backend */ + if (git_odb__backend_loose(&loose, objects_dir, &loose_opts) < 0 || + add_backend_internal(db, loose, git_odb__loose_priority, as_alternates, inode) < 0) + return -1; + + /* add the packed file backend */ +#ifdef GIT_EXPERIMENTAL_SHA256 + if (git_odb_backend_pack(&packed, objects_dir, &pack_opts) < 0) + return -1; +#else + GIT_UNUSED(pack_opts); + + if (git_odb_backend_pack(&packed, objects_dir) < 0) + return -1; +#endif + + if (add_backend_internal(db, packed, git_odb__packed_priority, as_alternates, inode) < 0) + return -1; + + if (git_mutex_lock(&db->lock) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return -1; + } + if (!db->cgraph && + git_commit_graph_new(&db->cgraph, objects_dir, false, db->options.oid_type) < 0) { + git_mutex_unlock(&db->lock); + return -1; + } + git_mutex_unlock(&db->lock); + + return load_alternates(db, objects_dir, alternate_depth); +} + +static int load_alternates(git_odb *odb, const char *objects_dir, int alternate_depth) +{ + git_str alternates_path = GIT_STR_INIT; + git_str alternates_buf = GIT_STR_INIT; + char *buffer; + const char *alternate; + int result = 0; + + /* Git reports an error, we just ignore anything deeper */ + if (alternate_depth > GIT_ALTERNATES_MAX_DEPTH) + return 0; + + if (git_str_joinpath(&alternates_path, objects_dir, GIT_ALTERNATES_FILE) < 0) + return -1; + + if (git_fs_path_exists(alternates_path.ptr) == false) { + git_str_dispose(&alternates_path); + return 0; + } + + if (git_futils_readbuffer(&alternates_buf, alternates_path.ptr) < 0) { + git_str_dispose(&alternates_path); + return -1; + } + + buffer = (char *)alternates_buf.ptr; + + /* add each alternate as a new backend; one alternate per line */ + while ((alternate = git__strtok(&buffer, "\r\n")) != NULL) { + if (*alternate == '\0' || *alternate == '#') + continue; + + /* + * Relative path: build based on the current `objects` + * folder. However, relative paths are only allowed in + * the current repository. + */ + if (*alternate == '.' && !alternate_depth) { + if ((result = git_str_joinpath(&alternates_path, objects_dir, alternate)) < 0) + break; + alternate = git_str_cstr(&alternates_path); + } + + if ((result = git_odb__add_default_backends(odb, alternate, true, alternate_depth + 1)) < 0) + break; + } + + git_str_dispose(&alternates_path); + git_str_dispose(&alternates_buf); + + return result; +} + +int git_odb_add_disk_alternate(git_odb *odb, const char *path) +{ + return git_odb__add_default_backends(odb, path, true, 0); +} + +int git_odb_set_commit_graph(git_odb *odb, git_commit_graph *cgraph) +{ + int error = 0; + + GIT_ASSERT_ARG(odb); + + if ((error = git_mutex_lock(&odb->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the db lock"); + return error; + } + git_commit_graph_free(odb->cgraph); + odb->cgraph = cgraph; + git_mutex_unlock(&odb->lock); + + return error; +} + +int git_odb__open( + git_odb **out, + const char *objects_dir, + const git_odb_options *opts) +{ + git_odb *db; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(objects_dir); + + *out = NULL; + + if (git_odb__new(&db, opts) < 0) + return -1; + + if (git_odb__add_default_backends(db, objects_dir, 0, 0) < 0) { + git_odb_free(db); + return -1; + } + + *out = db; + return 0; +} + +#ifdef GIT_EXPERIMENTAL_SHA256 + +int git_odb_open( + git_odb **out, + const char *objects_dir, + const git_odb_options *opts) +{ + return git_odb__open(out, objects_dir, opts); +} + +#else + +int git_odb_open(git_odb **out, const char *objects_dir) +{ + return git_odb__open(out, objects_dir, NULL); +} + +#endif + +int git_odb__set_caps(git_odb *odb, int caps) +{ + if (caps == GIT_ODB_CAP_FROM_OWNER) { + git_repository *repo = GIT_REFCOUNT_OWNER(odb); + int val; + + if (!repo) { + git_error_set(GIT_ERROR_ODB, "cannot access repository to set odb caps"); + return -1; + } + + if (!git_repository__configmap_lookup(&val, repo, GIT_CONFIGMAP_FSYNCOBJECTFILES)) + odb->do_fsync = !!val; + } + + return 0; +} + +static void odb_free(git_odb *db) +{ + size_t i; + bool locked = true; + + if (git_mutex_lock(&db->lock) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + locked = false; + } + for (i = 0; i < db->backends.length; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *backend = internal->backend; + + backend->free(backend); + + git__free(internal); + } + if (locked) + git_mutex_unlock(&db->lock); + + git_commit_graph_free(db->cgraph); + git_vector_free(&db->backends); + git_cache_dispose(&db->own_cache); + git_mutex_free(&db->lock); + + git__memzero(db, sizeof(*db)); + git__free(db); +} + +void git_odb_free(git_odb *db) +{ + if (db == NULL) + return; + + GIT_REFCOUNT_DEC(db, odb_free); +} + +static int odb_exists_1( + git_odb *db, + const git_oid *id, + bool only_refreshed) +{ + size_t i; + bool found = false; + int error; + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + for (i = 0; i < db->backends.length && !found; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + if (only_refreshed && !b->refresh) + continue; + + if (b->exists != NULL) + found = (bool)b->exists(b, id); + } + git_mutex_unlock(&db->lock); + + return (int)found; +} + +int git_odb__get_commit_graph_file(git_commit_graph_file **out, git_odb *db) +{ + int error = 0; + git_commit_graph_file *result = NULL; + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the db lock"); + return error; + } + if (!db->cgraph) { + error = GIT_ENOTFOUND; + goto done; + } + error = git_commit_graph_get_file(&result, db->cgraph); + if (error) + goto done; + *out = result; + +done: + git_mutex_unlock(&db->lock); + return error; +} + +static int odb_freshen_1( + git_odb *db, + const git_oid *id, + bool only_refreshed) +{ + size_t i; + bool found = false; + int error; + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + for (i = 0; i < db->backends.length && !found; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + if (only_refreshed && !b->refresh) + continue; + + if (b->freshen != NULL) + found = !b->freshen(b, id); + else if (b->exists != NULL) + found = b->exists(b, id); + } + git_mutex_unlock(&db->lock); + + return (int)found; +} + +int git_odb__freshen(git_odb *db, const git_oid *id) +{ + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(id); + + if (odb_freshen_1(db, id, false)) + return 1; + + if (!git_odb_refresh(db)) + return odb_freshen_1(db, id, true); + + /* Failed to refresh, hence not found */ + return 0; +} + +int git_odb_exists(git_odb *db, const git_oid *id) +{ + return git_odb_exists_ext(db, id, 0); +} + +int git_odb_exists_ext(git_odb *db, const git_oid *id, unsigned int flags) +{ + git_odb_object *object; + + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(id); + + if (git_oid_is_zero(id)) + return 0; + + if ((object = git_cache_get_raw(odb_cache(db), id)) != NULL) { + git_odb_object_free(object); + return 1; + } + + if (odb_exists_1(db, id, false)) + return 1; + + if (!(flags & GIT_ODB_LOOKUP_NO_REFRESH) && !git_odb_refresh(db)) + return odb_exists_1(db, id, true); + + /* Failed to refresh, hence not found */ + return 0; +} + +static int odb_exists_prefix_1(git_oid *out, git_odb *db, + const git_oid *key, size_t len, bool only_refreshed) +{ + size_t i; + int error = GIT_ENOTFOUND, num_found = 0; + git_oid last_found = GIT_OID_NONE, found; + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + error = GIT_ENOTFOUND; + for (i = 0; i < db->backends.length; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + if (only_refreshed && !b->refresh) + continue; + + if (!b->exists_prefix) + continue; + + error = b->exists_prefix(&found, b, key, len); + if (error == GIT_ENOTFOUND || error == GIT_PASSTHROUGH) + continue; + if (error) { + git_mutex_unlock(&db->lock); + return error; + } + + /* make sure found item doesn't introduce ambiguity */ + if (num_found) { + if (git_oid__cmp(&last_found, &found)) { + git_mutex_unlock(&db->lock); + return git_odb__error_ambiguous("multiple matches for prefix"); + } + } else { + git_oid_cpy(&last_found, &found); + num_found++; + } + } + git_mutex_unlock(&db->lock); + + if (!num_found) + return GIT_ENOTFOUND; + + if (out) + git_oid_cpy(out, &last_found); + + return 0; +} + +int git_odb_exists_prefix( + git_oid *out, git_odb *db, const git_oid *short_id, size_t len) +{ + int error; + git_oid key = GIT_OID_NONE; + + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(short_id); + + if (len < GIT_OID_MINPREFIXLEN) + return git_odb__error_ambiguous("prefix length too short"); + + if (len >= git_oid_hexsize(db->options.oid_type)) { + if (git_odb_exists(db, short_id)) { + if (out) + git_oid_cpy(out, short_id); + return 0; + } else { + return git_odb__error_notfound( + "no match for id prefix", short_id, len); + } + } + + git_oid__cpy_prefix(&key, short_id, len); + + error = odb_exists_prefix_1(out, db, &key, len, false); + + if (error == GIT_ENOTFOUND && !git_odb_refresh(db)) + error = odb_exists_prefix_1(out, db, &key, len, true); + + if (error == GIT_ENOTFOUND) + return git_odb__error_notfound("no match for id prefix", &key, len); + + return error; +} + +int git_odb_expand_ids( + git_odb *db, + git_odb_expand_id *ids, + size_t count) +{ + size_t hex_size, i; + + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(ids); + + hex_size = git_oid_hexsize(db->options.oid_type); + + for (i = 0; i < count; i++) { + git_odb_expand_id *query = &ids[i]; + int error = GIT_EAMBIGUOUS; + + if (!query->type) + query->type = GIT_OBJECT_ANY; + + /* if we have a short OID, expand it first */ + if (query->length >= GIT_OID_MINPREFIXLEN && query->length < hex_size) { + git_oid actual_id; + + error = odb_exists_prefix_1(&actual_id, db, &query->id, query->length, false); + if (!error) { + git_oid_cpy(&query->id, &actual_id); + query->length = (unsigned short)hex_size; + } + } + + /* + * now we ought to have a 40-char OID, either because we've expanded it + * or because the user passed a full OID. Ensure its type is right. + */ + if (query->length >= hex_size) { + git_object_t actual_type; + + error = odb_otype_fast(&actual_type, db, &query->id); + if (!error) { + if (query->type != GIT_OBJECT_ANY && query->type != actual_type) + error = GIT_ENOTFOUND; + else + query->type = actual_type; + } + } + + switch (error) { + /* no errors, so we've successfully expanded the OID */ + case 0: + continue; + + /* the object is missing or ambiguous */ + case GIT_ENOTFOUND: + case GIT_EAMBIGUOUS: + git_oid_clear(&query->id, db->options.oid_type); + query->length = 0; + query->type = 0; + break; + + /* something went very wrong with the ODB; bail hard */ + default: + return error; + } + } + + git_error_clear(); + return 0; +} + +int git_odb_read_header(size_t *len_p, git_object_t *type_p, git_odb *db, const git_oid *id) +{ + int error; + git_odb_object *object = NULL; + + error = git_odb__read_header_or_object(&object, len_p, type_p, db, id); + + if (object) + git_odb_object_free(object); + + return error; +} + +static int odb_read_header_1( + size_t *len_p, git_object_t *type_p, git_odb *db, + const git_oid *id, bool only_refreshed) +{ + size_t i; + git_object_t ht; + bool passthrough = false; + int error; + + if (!only_refreshed && (ht = odb_hardcoded_type(id)) != GIT_OBJECT_INVALID) { + *type_p = ht; + *len_p = 0; + return 0; + } + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + for (i = 0; i < db->backends.length; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + if (only_refreshed && !b->refresh) + continue; + + if (!b->read_header) { + passthrough = true; + continue; + } + + error = b->read_header(len_p, type_p, b, id); + + switch (error) { + case GIT_PASSTHROUGH: + passthrough = true; + break; + case GIT_ENOTFOUND: + break; + default: + git_mutex_unlock(&db->lock); + return error; + } + } + git_mutex_unlock(&db->lock); + + return passthrough ? GIT_PASSTHROUGH : GIT_ENOTFOUND; +} + +int git_odb__read_header_or_object( + git_odb_object **out, size_t *len_p, git_object_t *type_p, + git_odb *db, const git_oid *id) +{ + int error = GIT_ENOTFOUND; + git_odb_object *object; + + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(id); + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(len_p); + GIT_ASSERT_ARG(type_p); + + *out = NULL; + + if (git_oid_is_zero(id)) + return error_null_oid(GIT_ENOTFOUND, "cannot read object"); + + if ((object = git_cache_get_raw(odb_cache(db), id)) != NULL) { + *len_p = object->cached.size; + *type_p = object->cached.type; + *out = object; + return 0; + } + + error = odb_read_header_1(len_p, type_p, db, id, false); + + if (error == GIT_ENOTFOUND && !git_odb_refresh(db)) + error = odb_read_header_1(len_p, type_p, db, id, true); + + if (error == GIT_ENOTFOUND) + return git_odb__error_notfound("cannot read header for", id, git_oid_hexsize(db->options.oid_type)); + + /* we found the header; return early */ + if (!error) + return 0; + + if (error == GIT_PASSTHROUGH) { + /* + * no backend has header-reading functionality + * so try using `git_odb_read` instead + */ + error = git_odb_read(&object, db, id); + if (!error) { + *len_p = object->cached.size; + *type_p = object->cached.type; + *out = object; + } + } + + return error; +} + +static int odb_read_1( + git_odb_object **out, + git_odb *db, + const git_oid *id, + bool only_refreshed) +{ + size_t i; + git_rawobj raw; + git_odb_object *object; + git_oid hashed; + bool found = false; + int error = 0; + + if (!only_refreshed) { + if ((error = odb_read_hardcoded(&found, &raw, id)) < 0) + return error; + } + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + for (i = 0; i < db->backends.length && !found; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + if (only_refreshed && !b->refresh) + continue; + + if (b->read != NULL) { + error = b->read(&raw.data, &raw.len, &raw.type, b, id); + if (error == GIT_PASSTHROUGH || error == GIT_ENOTFOUND) + continue; + + if (error < 0) { + git_mutex_unlock(&db->lock); + return error; + } + + found = true; + } + } + git_mutex_unlock(&db->lock); + + if (!found) + return GIT_ENOTFOUND; + + if (git_odb__strict_hash_verification) { + if ((error = git_odb__hash(&hashed, raw.data, raw.len, raw.type, db->options.oid_type)) < 0) + goto out; + + if (!git_oid_equal(id, &hashed)) { + error = git_odb__error_mismatch(id, &hashed); + goto out; + } + } + + git_error_clear(); + if ((object = odb_object__alloc(id, &raw)) == NULL) { + error = -1; + goto out; + } + + *out = git_cache_store_raw(odb_cache(db), object); + +out: + if (error) + git__free(raw.data); + return error; +} + +int git_odb_read(git_odb_object **out, git_odb *db, const git_oid *id) +{ + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(id); + + if (git_oid_is_zero(id)) + return error_null_oid(GIT_ENOTFOUND, "cannot read object"); + + *out = git_cache_get_raw(odb_cache(db), id); + if (*out != NULL) + return 0; + + error = odb_read_1(out, db, id, false); + + if (error == GIT_ENOTFOUND && !git_odb_refresh(db)) + error = odb_read_1(out, db, id, true); + + if (error == GIT_ENOTFOUND) + return git_odb__error_notfound("no match for id", id, git_oid_hexsize(git_oid_type(id))); + + return error; +} + +static int odb_otype_fast(git_object_t *type_p, git_odb *db, const git_oid *id) +{ + git_odb_object *object; + size_t _unused; + int error; + + if (git_oid_is_zero(id)) + return error_null_oid(GIT_ENOTFOUND, "cannot get object type"); + + if ((object = git_cache_get_raw(odb_cache(db), id)) != NULL) { + *type_p = object->cached.type; + git_odb_object_free(object); + return 0; + } + + error = odb_read_header_1(&_unused, type_p, db, id, false); + + if (error == GIT_PASSTHROUGH) { + error = odb_read_1(&object, db, id, false); + if (!error) + *type_p = object->cached.type; + git_odb_object_free(object); + } + + return error; +} + +static int read_prefix_1(git_odb_object **out, git_odb *db, + const git_oid *key, size_t len, bool only_refreshed) +{ + size_t i; + int error = 0; + git_oid found_full_oid = GIT_OID_NONE; + git_rawobj raw = {0}; + void *data = NULL; + bool found = false; + git_odb_object *object; + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + for (i = 0; i < db->backends.length; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + if (only_refreshed && !b->refresh) + continue; + + if (b->read_prefix != NULL) { + git_oid full_oid; + error = b->read_prefix(&full_oid, &raw.data, &raw.len, &raw.type, b, key, len); + + if (error == GIT_ENOTFOUND || error == GIT_PASSTHROUGH) { + error = 0; + continue; + } + + if (error) { + git_mutex_unlock(&db->lock); + goto out; + } + + git__free(data); + data = raw.data; + + if (found && git_oid__cmp(&full_oid, &found_full_oid)) { + git_str buf = GIT_STR_INIT; + const char *idstr; + + if ((idstr = git_oid_tostr_s(&full_oid)) == NULL) { + git_str_puts(&buf, "failed to parse object id"); + } else { + git_str_printf(&buf, "multiple matches for prefix: %s", idstr); + + if ((idstr = git_oid_tostr_s(&found_full_oid)) != NULL) + git_str_printf(&buf, " %s", idstr); + } + + error = git_odb__error_ambiguous(buf.ptr); + git_str_dispose(&buf); + git_mutex_unlock(&db->lock); + goto out; + } + + found_full_oid = full_oid; + found = true; + } + } + git_mutex_unlock(&db->lock); + + if (!found) + return GIT_ENOTFOUND; + + if (git_odb__strict_hash_verification) { + git_oid hash; + + if ((error = git_odb__hash(&hash, raw.data, raw.len, raw.type, db->options.oid_type)) < 0) + goto out; + + if (!git_oid_equal(&found_full_oid, &hash)) { + error = git_odb__error_mismatch(&found_full_oid, &hash); + goto out; + } + } + + if ((object = odb_object__alloc(&found_full_oid, &raw)) == NULL) { + error = -1; + goto out; + } + + *out = git_cache_store_raw(odb_cache(db), object); + +out: + if (error) + git__free(raw.data); + + return error; +} + +int git_odb_read_prefix( + git_odb_object **out, git_odb *db, const git_oid *short_id, size_t len) +{ + git_oid key = GIT_OID_NONE; + size_t hex_size; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(db); + + hex_size = git_oid_hexsize(db->options.oid_type); + + if (len < GIT_OID_MINPREFIXLEN) + return git_odb__error_ambiguous("prefix length too short"); + + if (len > hex_size) + len = hex_size; + + if (len == hex_size) { + *out = git_cache_get_raw(odb_cache(db), short_id); + if (*out != NULL) + return 0; + } + + git_oid__cpy_prefix(&key, short_id, len); + + error = read_prefix_1(out, db, &key, len, false); + + if (error == GIT_ENOTFOUND && !git_odb_refresh(db)) + error = read_prefix_1(out, db, &key, len, true); + + if (error == GIT_ENOTFOUND) + return git_odb__error_notfound("no match for prefix", &key, len); + + return error; +} + +int git_odb_foreach(git_odb *db, git_odb_foreach_cb cb, void *payload) +{ + unsigned int i; + git_vector backends = GIT_VECTOR_INIT; + backend_internal *internal; + int error = 0; + + /* Make a copy of the backends vector to invoke the callback without holding the lock. */ + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + goto cleanup; + } + error = git_vector_dup(&backends, &db->backends, NULL); + git_mutex_unlock(&db->lock); + + if (error < 0) + goto cleanup; + + git_vector_foreach(&backends, i, internal) { + git_odb_backend *b = internal->backend; + error = b->foreach(b, cb, payload); + if (error != 0) + goto cleanup; + } + +cleanup: + git_vector_free(&backends); + + return error; +} + +int git_odb_write( + git_oid *oid, git_odb *db, const void *data, size_t len, git_object_t type) +{ + size_t i; + int error; + git_odb_stream *stream; + + GIT_ASSERT_ARG(oid); + GIT_ASSERT_ARG(db); + + if ((error = git_odb__hash(oid, data, len, type, db->options.oid_type)) < 0) + return error; + + if (git_oid_is_zero(oid)) + return error_null_oid(GIT_EINVALID, "cannot write object"); + + if (git_odb__freshen(db, oid)) + return 0; + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + for (i = 0, error = GIT_ERROR; i < db->backends.length && error < 0; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + /* we don't write in alternates! */ + if (internal->is_alternate) + continue; + + if (b->write != NULL) + error = b->write(b, oid, data, len, type); + } + git_mutex_unlock(&db->lock); + + if (!error || error == GIT_PASSTHROUGH) + return 0; + + /* if no backends were able to write the object directly, we try a + * streaming write to the backends; just write the whole object into the + * stream in one push + */ + if ((error = git_odb_open_wstream(&stream, db, len, type)) != 0) + return error; + + if ((error = stream->write(stream, data, len)) == 0) + error = stream->finalize_write(stream, oid); + + git_odb_stream_free(stream); + return error; +} + +static int hash_header(git_hash_ctx *ctx, git_object_size_t size, git_object_t type) +{ + char header[64]; + size_t hdrlen; + int error; + + if ((error = git_odb__format_object_header(&hdrlen, + header, sizeof(header), size, type)) < 0) + return error; + + return git_hash_update(ctx, header, hdrlen); +} + +int git_odb_open_wstream( + git_odb_stream **stream, git_odb *db, git_object_size_t size, git_object_t type) +{ + size_t i, writes = 0; + int error = GIT_ERROR; + git_hash_ctx *ctx = NULL; + + GIT_ASSERT_ARG(stream); + GIT_ASSERT_ARG(db); + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + error = GIT_ERROR; + for (i = 0; i < db->backends.length && error < 0; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + /* we don't write in alternates! */ + if (internal->is_alternate) + continue; + + if (b->writestream != NULL) { + ++writes; + error = b->writestream(stream, b, size, type); + } else if (b->write != NULL) { + ++writes; + error = init_fake_wstream(stream, b, size, type); + } + } + git_mutex_unlock(&db->lock); + + if (error < 0) { + if (error == GIT_PASSTHROUGH) + error = 0; + else if (!writes) + error = git_odb__error_unsupported_in_backend("write object"); + + goto done; + } + + ctx = git__malloc(sizeof(git_hash_ctx)); + GIT_ERROR_CHECK_ALLOC(ctx); + + if ((error = git_hash_ctx_init(ctx, git_oid_algorithm(db->options.oid_type))) < 0 || + (error = hash_header(ctx, size, type)) < 0) + goto done; + +#ifdef GIT_EXPERIMENTAL_SHA256 + (*stream)->oid_type = db->options.oid_type; +#endif + (*stream)->hash_ctx = ctx; + (*stream)->declared_size = size; + (*stream)->received_bytes = 0; + +done: + if (error) + git__free(ctx); + return error; +} + +static int git_odb_stream__invalid_length( + const git_odb_stream *stream, + const char *action) +{ + git_error_set(GIT_ERROR_ODB, + "cannot %s - " + "Invalid length. %"PRId64" was expected. The " + "total size of the received chunks amounts to %"PRId64".", + action, stream->declared_size, stream->received_bytes); + + return -1; +} + +int git_odb_stream_write(git_odb_stream *stream, const char *buffer, size_t len) +{ + git_hash_update(stream->hash_ctx, buffer, len); + + stream->received_bytes += len; + + if (stream->received_bytes > stream->declared_size) + return git_odb_stream__invalid_length(stream, + "stream_write()"); + + return stream->write(stream, buffer, len); +} + +int git_odb_stream_finalize_write(git_oid *out, git_odb_stream *stream) +{ + if (stream->received_bytes != stream->declared_size) + return git_odb_stream__invalid_length(stream, + "stream_finalize_write()"); + + git_hash_final(out->id, stream->hash_ctx); + +#ifdef GIT_EXPERIMENTAL_SHA256 + out->type = stream->oid_type; +#endif + + if (git_odb__freshen(stream->backend->odb, out)) + return 0; + + return stream->finalize_write(stream, out); +} + +int git_odb_stream_read(git_odb_stream *stream, char *buffer, size_t len) +{ + return stream->read(stream, buffer, len); +} + +void git_odb_stream_free(git_odb_stream *stream) +{ + if (stream == NULL) + return; + + git_hash_ctx_cleanup(stream->hash_ctx); + git__free(stream->hash_ctx); + stream->free(stream); +} + +int git_odb_open_rstream( + git_odb_stream **stream, + size_t *len, + git_object_t *type, + git_odb *db, + const git_oid *oid) +{ + size_t i, reads = 0; + int error = GIT_ERROR; + + GIT_ASSERT_ARG(stream); + GIT_ASSERT_ARG(db); + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + error = GIT_ERROR; + for (i = 0; i < db->backends.length && error < 0; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + if (b->readstream != NULL) { + ++reads; + error = b->readstream(stream, len, type, b, oid); + } + } + git_mutex_unlock(&db->lock); + + if (error == GIT_PASSTHROUGH) + error = 0; + if (error < 0 && !reads) + error = git_odb__error_unsupported_in_backend("read object streamed"); + + return error; +} + +int git_odb_write_pack(struct git_odb_writepack **out, git_odb *db, git_indexer_progress_cb progress_cb, void *progress_payload) +{ + size_t i, writes = 0; + int error = GIT_ERROR; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(db); + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + error = GIT_ERROR; + for (i = 0; i < db->backends.length && error < 0; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + /* we don't write in alternates! */ + if (internal->is_alternate) + continue; + + if (b->writepack != NULL) { + ++writes; + error = b->writepack(out, b, db, progress_cb, progress_payload); + } + } + git_mutex_unlock(&db->lock); + + if (error == GIT_PASSTHROUGH) + error = 0; + if (error < 0 && !writes) + error = git_odb__error_unsupported_in_backend("write pack"); + + return error; +} + +int git_odb_write_multi_pack_index(git_odb *db) +{ + size_t i, writes = 0; + int error = GIT_ERROR; + + GIT_ASSERT_ARG(db); + + for (i = 0; i < db->backends.length && error < 0; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + /* we don't write in alternates! */ + if (internal->is_alternate) + continue; + + if (b->writemidx != NULL) { + ++writes; + error = b->writemidx(b); + } + } + + if (error == GIT_PASSTHROUGH) + error = 0; + if (error < 0 && !writes) + error = git_odb__error_unsupported_in_backend("write multi-pack-index"); + + return error; +} + +void *git_odb_backend_data_alloc(git_odb_backend *backend, size_t len) +{ + GIT_UNUSED(backend); + return git__malloc(len); +} + +#ifndef GIT_DEPRECATE_HARD +void *git_odb_backend_malloc(git_odb_backend *backend, size_t len) +{ + return git_odb_backend_data_alloc(backend, len); +} +#endif + +void git_odb_backend_data_free(git_odb_backend *backend, void *data) +{ + GIT_UNUSED(backend); + git__free(data); +} + +int git_odb_refresh(struct git_odb *db) +{ + size_t i; + int error; + + GIT_ASSERT_ARG(db); + + if ((error = git_mutex_lock(&db->lock)) < 0) { + git_error_set(GIT_ERROR_ODB, "failed to acquire the odb lock"); + return error; + } + for (i = 0; i < db->backends.length; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + if (b->refresh != NULL) { + int error = b->refresh(b); + if (error < 0) { + git_mutex_unlock(&db->lock); + return error; + } + } + } + if (db->cgraph) + git_commit_graph_refresh(db->cgraph); + git_mutex_unlock(&db->lock); + + return 0; +} + +int git_odb__error_mismatch(const git_oid *expected, const git_oid *actual) +{ + char expected_oid[GIT_OID_MAX_HEXSIZE + 1], + actual_oid[GIT_OID_MAX_HEXSIZE + 1]; + + git_oid_tostr(expected_oid, git_oid_hexsize(git_oid_type(expected)) + 1, expected); + git_oid_tostr(actual_oid, git_oid_hexsize(git_oid_type(actual)) + 1, actual); + + git_error_set(GIT_ERROR_ODB, "object hash mismatch - expected %s but got %s", + expected_oid, actual_oid); + + return GIT_EMISMATCH; +} + +int git_odb__error_notfound( + const char *message, const git_oid *oid, size_t oid_len) +{ + if (oid != NULL) { + char oid_str[GIT_OID_MAX_HEXSIZE + 1]; + git_oid_tostr(oid_str, oid_len+1, oid); + git_error_set(GIT_ERROR_ODB, "object not found - %s (%.*s)", + message, (int) oid_len, oid_str); + } else + git_error_set(GIT_ERROR_ODB, "object not found - %s", message); + + return GIT_ENOTFOUND; +} + +static int error_null_oid(int error, const char *message) +{ + git_error_set(GIT_ERROR_ODB, "odb: %s: null OID cannot exist", message); + return error; +} + +int git_odb__error_ambiguous(const char *message) +{ + git_error_set(GIT_ERROR_ODB, "ambiguous OID prefix - %s", message); + return GIT_EAMBIGUOUS; +} + +int git_odb_init_backend(git_odb_backend *backend, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + backend, version, git_odb_backend, GIT_ODB_BACKEND_INIT); + return 0; +} diff --git a/src/libgit2/odb.h b/src/libgit2/odb.h new file mode 100644 index 0000000..7a712e2 --- /dev/null +++ b/src/libgit2/odb.h @@ -0,0 +1,188 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_odb_h__ +#define INCLUDE_odb_h__ + +#include "common.h" + +#include "git2/odb.h" +#include "git2/odb_backend.h" +#include "git2/oid.h" +#include "git2/types.h" +#include "git2/sys/commit_graph.h" + +#include "cache.h" +#include "commit_graph.h" +#include "filter.h" +#include "posix.h" +#include "vector.h" + +#define GIT_OBJECTS_DIR "objects/" +#define GIT_OBJECT_DIR_MODE 0777 +#define GIT_OBJECT_FILE_MODE 0444 + +#define GIT_ODB_DEFAULT_LOOSE_PRIORITY 1 +#define GIT_ODB_DEFAULT_PACKED_PRIORITY 2 + +extern bool git_odb__strict_hash_verification; + +/* DO NOT EXPORT */ +typedef struct { + void *data; /**< Raw, decompressed object data. */ + size_t len; /**< Total number of bytes in data. */ + git_object_t type; /**< Type of this object. */ +} git_rawobj; + +/* EXPORT */ +struct git_odb_object { + git_cached_obj cached; + void *buffer; +}; + +/* EXPORT */ +struct git_odb { + git_refcount rc; + git_mutex lock; /* protects backends */ + git_odb_options options; + git_vector backends; + git_cache own_cache; + git_commit_graph *cgraph; + unsigned int do_fsync :1; +}; + +typedef enum { + GIT_ODB_CAP_FROM_OWNER = -1 +} git_odb_cap_t; + +/* + * Set the capabilities for the object database. + */ +int git_odb__set_caps(git_odb *odb, int caps); + +/* + * Add the default loose and packed backends for a database. + */ +int git_odb__add_default_backends( + git_odb *db, const char *objects_dir, + bool as_alternates, int alternate_depth); + +/* + * Hash a git_rawobj internally. + * The `git_rawobj` is supposed to be previously initialized + */ +int git_odb__hashobj(git_oid *id, git_rawobj *obj, git_oid_t oid_type); + +/* + * Format the object header such as it would appear in the on-disk object + */ +int git_odb__format_object_header(size_t *out_len, char *hdr, size_t hdr_size, git_object_size_t obj_len, git_object_t obj_type); + +/* + * Hash an open file descriptor. + * This is a performance call when the contents of a fd need to be hashed, + * but the fd is already open and we have the size of the contents. + * + * Saves us some `stat` calls. + * + * The fd is never closed, not even on error. It must be opened and closed + * by the caller + */ +int git_odb__hashfd( + git_oid *out, + git_file fd, + size_t size, + git_object_t object_type, + git_oid_t oid_type); + +/* + * Hash an open file descriptor applying an array of filters + * Acts just like git_odb__hashfd with the addition of filters... + */ +int git_odb__hashfd_filtered( + git_oid *out, + git_file fd, + size_t len, + git_object_t object_type, + git_oid_t oid_type, + git_filter_list *fl); + +/* + * Hash a `path`, assuming it could be a POSIX symlink: if the path is a + * symlink, then the raw contents of the symlink will be hashed. Otherwise, + * this will fallback to `git_odb__hashfd`. + * + * The hash type for this call is always `GIT_OBJECT_BLOB` because + * symlinks may only point to blobs. + */ +int git_odb__hashlink(git_oid *out, const char *path, git_oid_t oid_type); + +/** + * Generate a GIT_EMISMATCH error for the ODB. + */ +int git_odb__error_mismatch( + const git_oid *expected, const git_oid *actual); + +/* + * Generate a GIT_ENOTFOUND error for the ODB. + */ +int git_odb__error_notfound( + const char *message, const git_oid *oid, size_t oid_len); + +/* + * Generate a GIT_EAMBIGUOUS error for the ODB. + */ +int git_odb__error_ambiguous(const char *message); + +/* + * Attempt to read object header or just return whole object if it could + * not be read. + */ +int git_odb__read_header_or_object( + git_odb_object **out, size_t *len_p, git_object_t *type_p, + git_odb *db, const git_oid *id); + +/* + * Attempt to get the ODB's commit-graph file. This object is still owned by + * the ODB. If the repository does not contain a commit-graph, it will return + * GIT_ENOTFOUND. + */ +int git_odb__get_commit_graph_file(git_commit_graph_file **out, git_odb *odb); + +/* freshen an entry in the object database */ +int git_odb__freshen(git_odb *db, const git_oid *id); + +/* fully free the object; internal method, DO NOT EXPORT */ +void git_odb_object__free(void *object); + +/* SHA256 support */ + +int git_odb__new(git_odb **out, const git_odb_options *opts); + +int git_odb__open( + git_odb **out, + const char *objects_dir, + const git_odb_options *opts); + +int git_odb__hash( + git_oid *out, + const void *data, + size_t len, + git_object_t object_type, + git_oid_t oid_type); + +int git_odb__hashfile( + git_oid *out, + const char *path, + git_object_t object_type, + git_oid_t oid_type); + +GIT_EXTERN(int) git_odb__backend_loose( + git_odb_backend **out, + const char *objects_dir, + git_odb_backend_loose_options *opts); + +#endif diff --git a/src/libgit2/odb_loose.c b/src/libgit2/odb_loose.c new file mode 100644 index 0000000..51195d3 --- /dev/null +++ b/src/libgit2/odb_loose.c @@ -0,0 +1,1240 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include +#include "git2/object.h" +#include "git2/sys/odb_backend.h" +#include "futils.h" +#include "hash.h" +#include "odb.h" +#include "delta.h" +#include "filebuf.h" +#include "object.h" +#include "zstream.h" + +#include "git2/odb_backend.h" +#include "git2/types.h" + +/* maximum possible header length */ +#define MAX_HEADER_LEN 64 + +typedef struct { /* object header data */ + git_object_t type; /* object type */ + size_t size; /* object size */ +} obj_hdr; + +typedef struct { + git_odb_stream stream; + git_filebuf fbuf; +} loose_writestream; + +typedef struct { + git_odb_stream stream; + git_map map; + char start[MAX_HEADER_LEN]; + size_t start_len; + size_t start_read; + git_zstream zstream; +} loose_readstream; + +typedef struct loose_backend { + git_odb_backend parent; + + git_odb_backend_loose_options options; + size_t oid_hexsize; + + size_t objects_dirlen; + char objects_dir[GIT_FLEX_ARRAY]; +} loose_backend; + +/* State structure for exploring directories, + * in order to locate objects matching a short oid. + */ +typedef struct { + loose_backend *backend; + + size_t dir_len; + + /* Hex formatted oid to match (and its length) */ + unsigned char short_oid[GIT_OID_MAX_HEXSIZE]; + size_t short_oid_len; + + /* Number of matching objects found so far */ + int found; + + /* Hex formatted oid of the object found */ + unsigned char res_oid[GIT_OID_MAX_HEXSIZE]; +} loose_locate_object_state; + + +/*********************************************************** + * + * MISCELLANEOUS HELPER FUNCTIONS + * + ***********************************************************/ + +static int object_file_name( + git_str *name, const loose_backend *be, const git_oid *id) +{ + /* append loose object filename: aa/aaa... (41 bytes plus NUL) */ + size_t path_size = be->oid_hexsize + 1; + + git_str_set(name, be->objects_dir, be->objects_dirlen); + git_fs_path_to_dir(name); + + if (git_str_grow_by(name, path_size + 1) < 0) + return -1; + + git_oid_pathfmt(name->ptr + name->size, id); + name->size += path_size; + name->ptr[name->size] = '\0'; + + return 0; +} + +static int object_mkdir(const git_str *name, const loose_backend *be) +{ + return git_futils_mkdir_relative( + name->ptr + be->objects_dirlen, + be->objects_dir, + be->options.dir_mode, + GIT_MKDIR_PATH | GIT_MKDIR_SKIP_LAST | GIT_MKDIR_VERIFY_DIR, NULL); +} + +static int parse_header_packlike( + obj_hdr *out, size_t *out_len, const unsigned char *data, size_t len) +{ + unsigned long c; + size_t shift, size, used = 0; + + if (len == 0) + goto on_error; + + c = data[used++]; + out->type = (c >> 4) & 7; + + size = c & 15; + shift = 4; + while (c & 0x80) { + if (len <= used) + goto on_error; + + if (sizeof(size_t) * 8 <= shift) + goto on_error; + + c = data[used++]; + size += (c & 0x7f) << shift; + shift += 7; + } + + out->size = size; + + if (out_len) + *out_len = used; + + return 0; + +on_error: + git_error_set(GIT_ERROR_OBJECT, "failed to parse loose object: invalid header"); + return -1; +} + +static int parse_header( + obj_hdr *out, + size_t *out_len, + const unsigned char *_data, + size_t data_len) +{ + const char *data = (char *)_data; + size_t i, typename_len, size_idx, size_len; + int64_t size; + + *out_len = 0; + + /* find the object type name */ + for (i = 0, typename_len = 0; i < data_len; i++, typename_len++) { + if (data[i] == ' ') + break; + } + + if (typename_len == data_len) + goto on_error; + + out->type = git_object_stringn2type(data, typename_len); + + size_idx = typename_len + 1; + for (i = size_idx, size_len = 0; i < data_len; i++, size_len++) { + if (data[i] == '\0') + break; + } + + if (i == data_len) + goto on_error; + + if (git__strntol64(&size, &data[size_idx], size_len, NULL, 10) < 0 || + size < 0) + goto on_error; + + if ((uint64_t)size > SIZE_MAX) { + git_error_set(GIT_ERROR_OBJECT, "object is larger than available memory"); + return -1; + } + + out->size = (size_t)size; + + if (GIT_ADD_SIZET_OVERFLOW(out_len, i, 1)) + goto on_error; + + return 0; + +on_error: + git_error_set(GIT_ERROR_OBJECT, "failed to parse loose object: invalid header"); + return -1; +} + +static int is_zlib_compressed_data(unsigned char *data, size_t data_len) +{ + unsigned int w; + + if (data_len < 2) + return 0; + + w = ((unsigned int)(data[0]) << 8) + data[1]; + return (data[0] & 0x8F) == 0x08 && !(w % 31); +} + +/*********************************************************** + * + * ODB OBJECT READING & WRITING + * + * Backend for the public API; read headers and full objects + * from the ODB. Write raw data to the ODB. + * + ***********************************************************/ + + +/* + * At one point, there was a loose object format that was intended to + * mimic the format used in pack-files. This was to allow easy copying + * of loose object data into packs. This format is no longer used, but + * we must still read it. + */ +static int read_loose_packlike(git_rawobj *out, git_str *obj) +{ + git_str body = GIT_STR_INIT; + const unsigned char *obj_data; + obj_hdr hdr; + size_t obj_len, head_len, alloc_size; + int error; + + obj_data = (unsigned char *)obj->ptr; + obj_len = obj->size; + + /* + * read the object header, which is an (uncompressed) + * binary encoding of the object type and size. + */ + if ((error = parse_header_packlike(&hdr, &head_len, obj_data, obj_len)) < 0) + goto done; + + if (!git_object_typeisloose(hdr.type) || head_len > obj_len) { + git_error_set(GIT_ERROR_ODB, "failed to inflate loose object"); + error = -1; + goto done; + } + + obj_data += head_len; + obj_len -= head_len; + + /* + * allocate a buffer and inflate the data into it + */ + if (GIT_ADD_SIZET_OVERFLOW(&alloc_size, hdr.size, 1) || + git_str_init(&body, alloc_size) < 0) { + error = -1; + goto done; + } + + if ((error = git_zstream_inflatebuf(&body, obj_data, obj_len)) < 0) + goto done; + + out->len = hdr.size; + out->type = hdr.type; + out->data = git_str_detach(&body); + +done: + git_str_dispose(&body); + return error; +} + +static int read_loose_standard(git_rawobj *out, git_str *obj) +{ + git_zstream zstream = GIT_ZSTREAM_INIT; + unsigned char head[MAX_HEADER_LEN], *body = NULL; + size_t decompressed, head_len, body_len, alloc_size; + obj_hdr hdr; + int error; + + if ((error = git_zstream_init(&zstream, GIT_ZSTREAM_INFLATE)) < 0 || + (error = git_zstream_set_input(&zstream, git_str_cstr(obj), git_str_len(obj))) < 0) + goto done; + + decompressed = sizeof(head); + + /* + * inflate the initial part of the compressed buffer in order to + * parse the header; read the largest header possible, then push the + * remainder into the body buffer. + */ + if ((error = git_zstream_get_output(head, &decompressed, &zstream)) < 0 || + (error = parse_header(&hdr, &head_len, head, decompressed)) < 0) + goto done; + + if (!git_object_typeisloose(hdr.type)) { + git_error_set(GIT_ERROR_ODB, "failed to inflate disk object"); + error = -1; + goto done; + } + + /* + * allocate a buffer and inflate the object data into it + * (including the initial sequence in the head buffer). + */ + if (GIT_ADD_SIZET_OVERFLOW(&alloc_size, hdr.size, 1) || + (body = git__calloc(1, alloc_size)) == NULL) { + error = -1; + goto done; + } + + GIT_ASSERT(decompressed >= head_len); + body_len = decompressed - head_len; + + if (body_len) + memcpy(body, head + head_len, body_len); + + decompressed = hdr.size - body_len; + if ((error = git_zstream_get_output(body + body_len, &decompressed, &zstream)) < 0) + goto done; + + if (!git_zstream_done(&zstream)) { + git_error_set(GIT_ERROR_ZLIB, "failed to finish zlib inflation: stream aborted prematurely"); + error = -1; + goto done; + } + + body[hdr.size] = '\0'; + + out->data = body; + out->len = hdr.size; + out->type = hdr.type; + +done: + if (error < 0) + git__free(body); + + git_zstream_free(&zstream); + return error; +} + +static int read_loose(git_rawobj *out, git_str *loc) +{ + int error; + git_str obj = GIT_STR_INIT; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(loc); + + if (git_str_oom(loc)) + return -1; + + out->data = NULL; + out->len = 0; + out->type = GIT_OBJECT_INVALID; + + if ((error = git_futils_readbuffer(&obj, loc->ptr)) < 0) + goto done; + + if (!is_zlib_compressed_data((unsigned char *)obj.ptr, obj.size)) + error = read_loose_packlike(out, &obj); + else + error = read_loose_standard(out, &obj); + +done: + git_str_dispose(&obj); + return error; +} + +static int read_header_loose_packlike( + git_rawobj *out, const unsigned char *data, size_t len) +{ + obj_hdr hdr; + size_t header_len; + int error; + + if ((error = parse_header_packlike(&hdr, &header_len, data, len)) < 0) + return error; + + out->len = hdr.size; + out->type = hdr.type; + + return error; +} + +static int read_header_loose_standard( + git_rawobj *out, const unsigned char *data, size_t len) +{ + git_zstream zs = GIT_ZSTREAM_INIT; + obj_hdr hdr = {0}; + unsigned char inflated[MAX_HEADER_LEN] = {0}; + size_t header_len, inflated_len = sizeof(inflated); + int error; + + if ((error = git_zstream_init(&zs, GIT_ZSTREAM_INFLATE)) < 0 || + (error = git_zstream_set_input(&zs, data, len)) < 0 || + (error = git_zstream_get_output_chunk(inflated, &inflated_len, &zs)) < 0 || + (error = parse_header(&hdr, &header_len, inflated, inflated_len)) < 0) + goto done; + + out->len = hdr.size; + out->type = hdr.type; + +done: + git_zstream_free(&zs); + return error; +} + +static int read_header_loose(git_rawobj *out, git_str *loc) +{ + unsigned char obj[1024]; + ssize_t obj_len; + int fd, error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(loc); + + if (git_str_oom(loc)) + return -1; + + out->data = NULL; + + if ((error = fd = git_futils_open_ro(loc->ptr)) < 0) + goto done; + + if ((obj_len = p_read(fd, obj, sizeof(obj))) < 0) { + error = (int)obj_len; + goto done; + } + + if (!is_zlib_compressed_data(obj, (size_t)obj_len)) + error = read_header_loose_packlike(out, obj, (size_t)obj_len); + else + error = read_header_loose_standard(out, obj, (size_t)obj_len); + + if (!error && !git_object_typeisloose(out->type)) { + git_error_set(GIT_ERROR_ZLIB, "failed to read loose object header"); + error = -1; + goto done; + } + +done: + if (fd >= 0) + p_close(fd); + return error; +} + +static int locate_object( + git_str *object_location, + loose_backend *backend, + const git_oid *oid) +{ + int error = object_file_name(object_location, backend, oid); + + if (!error && !git_fs_path_exists(object_location->ptr)) + return GIT_ENOTFOUND; + + return error; +} + +/* Explore an entry of a directory and see if it matches a short oid */ +static int fn_locate_object_short_oid(void *state, git_str *pathbuf) { + loose_locate_object_state *sstate = (loose_locate_object_state *)state; + size_t hex_size = sstate->backend->oid_hexsize; + + if (git_str_len(pathbuf) - sstate->dir_len != hex_size - 2) { + /* Entry cannot be an object. Continue to next entry */ + return 0; + } + + if (git_fs_path_isdir(pathbuf->ptr) == false) { + /* We are already in the directory matching the 2 first hex characters, + * compare the first ncmp characters of the oids */ + if (!memcmp(sstate->short_oid + 2, + (unsigned char *)pathbuf->ptr + sstate->dir_len, + sstate->short_oid_len - 2)) { + + if (!sstate->found) { + sstate->res_oid[0] = sstate->short_oid[0]; + sstate->res_oid[1] = sstate->short_oid[1]; + memcpy(sstate->res_oid + 2, + pathbuf->ptr+sstate->dir_len, + hex_size - 2); + } + sstate->found++; + } + } + + if (sstate->found > 1) + return GIT_EAMBIGUOUS; + + return 0; +} + +/* Locate an object matching a given short oid */ +static int locate_object_short_oid( + git_str *object_location, + git_oid *res_oid, + loose_backend *backend, + const git_oid *short_oid, + size_t len) +{ + char *objects_dir = backend->objects_dir; + size_t dir_len = strlen(objects_dir), alloc_len; + loose_locate_object_state state; + int error; + + /* prealloc memory for OBJ_DIR/xx/xx..38x..xx */ + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, dir_len, backend->oid_hexsize); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, 3); + if (git_str_grow(object_location, alloc_len) < 0) + return -1; + + git_str_set(object_location, objects_dir, dir_len); + git_fs_path_to_dir(object_location); + + /* save adjusted position at end of dir so it can be restored later */ + dir_len = git_str_len(object_location); + + /* Convert raw oid to hex formatted oid */ + git_oid_fmt((char *)state.short_oid, short_oid); + + /* Explore OBJ_DIR/xx/ where xx is the beginning of hex formatted short oid */ + if (git_str_put(object_location, (char *)state.short_oid, 3) < 0) + return -1; + object_location->ptr[object_location->size - 1] = '/'; + + /* Check that directory exists */ + if (git_fs_path_isdir(object_location->ptr) == false) + return git_odb__error_notfound("no matching loose object for prefix", + short_oid, len); + + state.backend = backend; + state.dir_len = git_str_len(object_location); + state.short_oid_len = len; + state.found = 0; + + /* Explore directory to find a unique object matching short_oid */ + error = git_fs_path_direach( + object_location, 0, fn_locate_object_short_oid, &state); + if (error < 0 && error != GIT_EAMBIGUOUS) + return error; + + if (!state.found) + return git_odb__error_notfound("no matching loose object for prefix", + short_oid, len); + + if (state.found > 1) + return git_odb__error_ambiguous("multiple matches in loose objects"); + + /* Convert obtained hex formatted oid to raw */ + error = git_oid__fromstr(res_oid, (char *)state.res_oid, backend->options.oid_type); + if (error) + return error; + + /* Update the location according to the oid obtained */ + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, dir_len, backend->oid_hexsize); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, 2); + + git_str_truncate(object_location, dir_len); + if (git_str_grow(object_location, alloc_len) < 0) + return -1; + + git_oid_pathfmt(object_location->ptr + dir_len, res_oid); + + object_location->size += backend->oid_hexsize + 1; + object_location->ptr[object_location->size] = '\0'; + + return 0; +} + +/*********************************************************** + * + * LOOSE BACKEND PUBLIC API + * + * Implement the git_odb_backend API calls + * + ***********************************************************/ + +static int loose_backend__read_header(size_t *len_p, git_object_t *type_p, git_odb_backend *backend, const git_oid *oid) +{ + git_str object_path = GIT_STR_INIT; + git_rawobj raw; + int error; + + GIT_ASSERT_ARG(backend); + GIT_ASSERT_ARG(oid); + + raw.len = 0; + raw.type = GIT_OBJECT_INVALID; + + if (locate_object(&object_path, (loose_backend *)backend, oid) < 0) { + error = git_odb__error_notfound("no matching loose object", + oid, ((struct loose_backend *)backend)->oid_hexsize); + } else if ((error = read_header_loose(&raw, &object_path)) == 0) { + *len_p = raw.len; + *type_p = raw.type; + } + + git_str_dispose(&object_path); + + return error; +} + +static int loose_backend__read(void **buffer_p, size_t *len_p, git_object_t *type_p, git_odb_backend *backend, const git_oid *oid) +{ + git_str object_path = GIT_STR_INIT; + git_rawobj raw; + int error = 0; + + GIT_ASSERT_ARG(backend); + GIT_ASSERT_ARG(oid); + + if (locate_object(&object_path, (loose_backend *)backend, oid) < 0) { + error = git_odb__error_notfound("no matching loose object", + oid, ((struct loose_backend *)backend)->oid_hexsize); + } else if ((error = read_loose(&raw, &object_path)) == 0) { + *buffer_p = raw.data; + *len_p = raw.len; + *type_p = raw.type; + } + + git_str_dispose(&object_path); + + return error; +} + +static int loose_backend__read_prefix( + git_oid *out_oid, + void **buffer_p, + size_t *len_p, + git_object_t *type_p, + git_odb_backend *_backend, + const git_oid *short_oid, + size_t len) +{ + struct loose_backend *backend = (struct loose_backend *)_backend; + int error = 0; + + GIT_ASSERT_ARG(len >= GIT_OID_MINPREFIXLEN && + len <= backend->oid_hexsize); + + if (len == backend->oid_hexsize) { + /* We can fall back to regular read method */ + error = loose_backend__read(buffer_p, len_p, type_p, _backend, short_oid); + if (!error) + git_oid_cpy(out_oid, short_oid); + } else { + git_str object_path = GIT_STR_INIT; + git_rawobj raw; + + GIT_ASSERT_ARG(backend && short_oid); + + if ((error = locate_object_short_oid(&object_path, out_oid, + (loose_backend *)backend, short_oid, len)) == 0 && + (error = read_loose(&raw, &object_path)) == 0) + { + *buffer_p = raw.data; + *len_p = raw.len; + *type_p = raw.type; + } + + git_str_dispose(&object_path); + } + + return error; +} + +static int loose_backend__exists(git_odb_backend *backend, const git_oid *oid) +{ + git_str object_path = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(backend); + GIT_ASSERT_ARG(oid); + + error = locate_object(&object_path, (loose_backend *)backend, oid); + + git_str_dispose(&object_path); + + return !error; +} + +static int loose_backend__exists_prefix( + git_oid *out, git_odb_backend *backend, const git_oid *short_id, size_t len) +{ + git_str object_path = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(backend); + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(short_id); + GIT_ASSERT_ARG(len >= GIT_OID_MINPREFIXLEN); + + error = locate_object_short_oid( + &object_path, out, (loose_backend *)backend, short_id, len); + + git_str_dispose(&object_path); + + return error; +} + +struct foreach_state { + struct loose_backend *backend; + size_t dir_len; + git_odb_foreach_cb cb; + void *data; +}; + +GIT_INLINE(int) filename_to_oid(struct loose_backend *backend, git_oid *oid, const char *ptr) +{ + int v; + size_t i = 0; + + if (strlen(ptr) != backend->oid_hexsize + 1) + return -1; + + if (ptr[2] != '/') { + return -1; + } + + v = (git__fromhex(ptr[i]) << 4) | git__fromhex(ptr[i+1]); + if (v < 0) + return -1; + + oid->id[0] = (unsigned char) v; + + ptr += 3; + for (i = 0; i < backend->oid_hexsize - 2; i += 2) { + v = (git__fromhex(ptr[i]) << 4) | git__fromhex(ptr[i + 1]); + if (v < 0) + return -1; + + oid->id[1 + i/2] = (unsigned char) v; + } + +#ifdef GIT_EXPERIMENTAL_SHA256 + oid->type = backend->options.oid_type; +#endif + + return 0; +} + +static int foreach_object_dir_cb(void *_state, git_str *path) +{ + git_oid oid; + struct foreach_state *state = (struct foreach_state *) _state; + + if (filename_to_oid(state->backend, &oid, path->ptr + state->dir_len) < 0) + return 0; + + return git_error_set_after_callback_function( + state->cb(&oid, state->data), "git_odb_foreach"); +} + +static int foreach_cb(void *_state, git_str *path) +{ + struct foreach_state *state = (struct foreach_state *) _state; + + /* non-dir is some stray file, ignore it */ + if (!git_fs_path_isdir(git_str_cstr(path))) + return 0; + + return git_fs_path_direach(path, 0, foreach_object_dir_cb, state); +} + +static int loose_backend__foreach(git_odb_backend *_backend, git_odb_foreach_cb cb, void *data) +{ + char *objects_dir; + int error; + git_str buf = GIT_STR_INIT; + struct foreach_state state; + loose_backend *backend = (loose_backend *) _backend; + + GIT_ASSERT_ARG(backend); + GIT_ASSERT_ARG(cb); + + objects_dir = backend->objects_dir; + + git_str_sets(&buf, objects_dir); + git_fs_path_to_dir(&buf); + if (git_str_oom(&buf)) + return -1; + + memset(&state, 0, sizeof(state)); + state.backend = backend; + state.cb = cb; + state.data = data; + state.dir_len = git_str_len(&buf); + + error = git_fs_path_direach(&buf, 0, foreach_cb, &state); + + git_str_dispose(&buf); + + return error; +} + +static int loose_backend__writestream_finalize(git_odb_stream *_stream, const git_oid *oid) +{ + loose_writestream *stream = (loose_writestream *)_stream; + loose_backend *backend = (loose_backend *)_stream->backend; + git_str final_path = GIT_STR_INIT; + int error = 0; + + if (object_file_name(&final_path, backend, oid) < 0 || + object_mkdir(&final_path, backend) < 0) + error = -1; + else + error = git_filebuf_commit_at( + &stream->fbuf, final_path.ptr); + + git_str_dispose(&final_path); + + return error; +} + +static int loose_backend__writestream_write(git_odb_stream *_stream, const char *data, size_t len) +{ + loose_writestream *stream = (loose_writestream *)_stream; + return git_filebuf_write(&stream->fbuf, data, len); +} + +static void loose_backend__writestream_free(git_odb_stream *_stream) +{ + loose_writestream *stream = (loose_writestream *)_stream; + + git_filebuf_cleanup(&stream->fbuf); + git__free(stream); +} + +static int filebuf_flags(loose_backend *backend) +{ + int flags = GIT_FILEBUF_TEMPORARY | + (backend->options.compression_level << GIT_FILEBUF_DEFLATE_SHIFT); + + if ((backend->options.flags & GIT_ODB_BACKEND_LOOSE_FSYNC) || + git_repository__fsync_gitdir) + flags |= GIT_FILEBUF_FSYNC; + + return flags; +} + +static int loose_backend__writestream(git_odb_stream **stream_out, git_odb_backend *_backend, git_object_size_t length, git_object_t type) +{ + loose_backend *backend; + loose_writestream *stream = NULL; + char hdr[MAX_HEADER_LEN]; + git_str tmp_path = GIT_STR_INIT; + size_t hdrlen; + int error; + + GIT_ASSERT_ARG(_backend); + + backend = (loose_backend *)_backend; + *stream_out = NULL; + + if ((error = git_odb__format_object_header(&hdrlen, + hdr, sizeof(hdr), length, type)) < 0) + return error; + + stream = git__calloc(1, sizeof(loose_writestream)); + GIT_ERROR_CHECK_ALLOC(stream); + + stream->stream.backend = _backend; + stream->stream.read = NULL; /* read only */ + stream->stream.write = &loose_backend__writestream_write; + stream->stream.finalize_write = &loose_backend__writestream_finalize; + stream->stream.free = &loose_backend__writestream_free; + stream->stream.mode = GIT_STREAM_WRONLY; + + if (git_str_joinpath(&tmp_path, backend->objects_dir, "tmp_object") < 0 || + git_filebuf_open(&stream->fbuf, tmp_path.ptr, filebuf_flags(backend), + backend->options.file_mode) < 0 || + stream->stream.write((git_odb_stream *)stream, hdr, hdrlen) < 0) + { + git_filebuf_cleanup(&stream->fbuf); + git__free(stream); + stream = NULL; + } + git_str_dispose(&tmp_path); + *stream_out = (git_odb_stream *)stream; + + return !stream ? -1 : 0; +} + +static int loose_backend__readstream_read( + git_odb_stream *_stream, + char *buffer, + size_t buffer_len) +{ + loose_readstream *stream = (loose_readstream *)_stream; + size_t start_remain = stream->start_len - stream->start_read; + int total = 0, error; + + buffer_len = min(buffer_len, INT_MAX); + + /* + * if we read more than just the header in the initial read, play + * that back for the caller. + */ + if (start_remain && buffer_len) { + size_t chunk = min(start_remain, buffer_len); + memcpy(buffer, stream->start + stream->start_read, chunk); + + buffer += chunk; + stream->start_read += chunk; + + total += (int)chunk; + buffer_len -= chunk; + } + + if (buffer_len) { + size_t chunk = buffer_len; + + if ((error = git_zstream_get_output(buffer, &chunk, &stream->zstream)) < 0) + return error; + + total += (int)chunk; + } + + return (int)total; +} + +static void loose_backend__readstream_free(git_odb_stream *_stream) +{ + loose_readstream *stream = (loose_readstream *)_stream; + + git_futils_mmap_free(&stream->map); + git_zstream_free(&stream->zstream); + git__free(stream); +} + +static int loose_backend__readstream_packlike( + obj_hdr *hdr, + loose_readstream *stream) +{ + const unsigned char *data; + size_t data_len, head_len; + int error; + + data = stream->map.data; + data_len = stream->map.len; + + /* + * read the object header, which is an (uncompressed) + * binary encoding of the object type and size. + */ + if ((error = parse_header_packlike(hdr, &head_len, data, data_len)) < 0) + return error; + + if (!git_object_typeisloose(hdr->type)) { + git_error_set(GIT_ERROR_ODB, "failed to inflate loose object"); + return -1; + } + + return git_zstream_set_input(&stream->zstream, + data + head_len, data_len - head_len); +} + +static int loose_backend__readstream_standard( + obj_hdr *hdr, + loose_readstream *stream) +{ + unsigned char head[MAX_HEADER_LEN]; + size_t init, head_len; + int error; + + if ((error = git_zstream_set_input(&stream->zstream, + stream->map.data, stream->map.len)) < 0) + return error; + + init = sizeof(head); + + /* + * inflate the initial part of the compressed buffer in order to + * parse the header; read the largest header possible, then store + * it in the `start` field of the stream object. + */ + if ((error = git_zstream_get_output(head, &init, &stream->zstream)) < 0 || + (error = parse_header(hdr, &head_len, head, init)) < 0) + return error; + + if (!git_object_typeisloose(hdr->type)) { + git_error_set(GIT_ERROR_ODB, "failed to inflate disk object"); + return -1; + } + + if (init > head_len) { + stream->start_len = init - head_len; + memcpy(stream->start, head + head_len, init - head_len); + } + + return 0; +} + +static int loose_backend__readstream( + git_odb_stream **stream_out, + size_t *len_out, + git_object_t *type_out, + git_odb_backend *_backend, + const git_oid *oid) +{ + loose_backend *backend; + loose_readstream *stream = NULL; + git_hash_ctx *hash_ctx = NULL; + git_str object_path = GIT_STR_INIT; + git_hash_algorithm_t algorithm; + obj_hdr hdr; + int error = 0; + + GIT_ASSERT_ARG(stream_out); + GIT_ASSERT_ARG(len_out); + GIT_ASSERT_ARG(type_out); + GIT_ASSERT_ARG(_backend); + GIT_ASSERT_ARG(oid); + + backend = (loose_backend *)_backend; + *stream_out = NULL; + *len_out = 0; + *type_out = GIT_OBJECT_INVALID; + + if (locate_object(&object_path, backend, oid) < 0) { + error = git_odb__error_notfound("no matching loose object", + oid, backend->oid_hexsize); + goto done; + } + + stream = git__calloc(1, sizeof(loose_readstream)); + GIT_ERROR_CHECK_ALLOC(stream); + + hash_ctx = git__malloc(sizeof(git_hash_ctx)); + GIT_ERROR_CHECK_ALLOC(hash_ctx); + + algorithm = git_oid_algorithm(backend->options.oid_type); + + if ((error = git_hash_ctx_init(hash_ctx, algorithm)) < 0 || + (error = git_futils_mmap_ro_file(&stream->map, object_path.ptr)) < 0 || + (error = git_zstream_init(&stream->zstream, GIT_ZSTREAM_INFLATE)) < 0) + goto done; + + /* check for a packlike loose object */ + if (!is_zlib_compressed_data(stream->map.data, stream->map.len)) + error = loose_backend__readstream_packlike(&hdr, stream); + else + error = loose_backend__readstream_standard(&hdr, stream); + + if (error < 0) + goto done; + + stream->stream.backend = _backend; + stream->stream.hash_ctx = hash_ctx; + stream->stream.read = &loose_backend__readstream_read; + stream->stream.free = &loose_backend__readstream_free; + + *stream_out = (git_odb_stream *)stream; + *len_out = hdr.size; + *type_out = hdr.type; + +done: + if (error < 0) { + if (stream) { + git_futils_mmap_free(&stream->map); + git_zstream_free(&stream->zstream); + git__free(stream); + } + if (hash_ctx) { + git_hash_ctx_cleanup(hash_ctx); + git__free(hash_ctx); + } + } + + git_str_dispose(&object_path); + return error; +} + +static int loose_backend__write(git_odb_backend *_backend, const git_oid *oid, const void *data, size_t len, git_object_t type) +{ + int error = 0; + git_str final_path = GIT_STR_INIT; + char header[MAX_HEADER_LEN]; + size_t header_len; + git_filebuf fbuf = GIT_FILEBUF_INIT; + loose_backend *backend; + + backend = (loose_backend *)_backend; + + /* prepare the header for the file */ + if ((error = git_odb__format_object_header(&header_len, + header, sizeof(header), len, type)) < 0) + goto cleanup; + + if (git_str_joinpath(&final_path, backend->objects_dir, "tmp_object") < 0 || + git_filebuf_open(&fbuf, final_path.ptr, filebuf_flags(backend), + backend->options.file_mode) < 0) + { + error = -1; + goto cleanup; + } + + git_filebuf_write(&fbuf, header, header_len); + git_filebuf_write(&fbuf, data, len); + + if (object_file_name(&final_path, backend, oid) < 0 || + object_mkdir(&final_path, backend) < 0 || + git_filebuf_commit_at(&fbuf, final_path.ptr) < 0) + error = -1; + +cleanup: + if (error < 0) + git_filebuf_cleanup(&fbuf); + git_str_dispose(&final_path); + return error; +} + +static int loose_backend__freshen( + git_odb_backend *_backend, + const git_oid *oid) +{ + loose_backend *backend = (loose_backend *)_backend; + git_str path = GIT_STR_INIT; + int error; + + if (object_file_name(&path, backend, oid) < 0) + return -1; + + error = git_futils_touch(path.ptr, NULL); + git_str_dispose(&path); + + return error; +} + +static void loose_backend__free(git_odb_backend *_backend) +{ + git__free(_backend); +} + +static void normalize_options( + git_odb_backend_loose_options *opts, + const git_odb_backend_loose_options *given_opts) +{ + git_odb_backend_loose_options init = GIT_ODB_BACKEND_LOOSE_OPTIONS_INIT; + + if (given_opts) + memcpy(opts, given_opts, sizeof(git_odb_backend_loose_options)); + else + memcpy(opts, &init, sizeof(git_odb_backend_loose_options)); + + if (opts->compression_level < 0) + opts->compression_level = Z_BEST_SPEED; + + if (opts->dir_mode == 0) + opts->dir_mode = GIT_OBJECT_DIR_MODE; + + if (opts->file_mode == 0) + opts->file_mode = GIT_OBJECT_FILE_MODE; + + if (opts->oid_type == 0) + opts->oid_type = GIT_OID_DEFAULT; +} + +int git_odb__backend_loose( + git_odb_backend **backend_out, + const char *objects_dir, + git_odb_backend_loose_options *opts) +{ + loose_backend *backend; + size_t objects_dirlen, alloclen; + + GIT_ASSERT_ARG(backend_out); + GIT_ASSERT_ARG(objects_dir); + + objects_dirlen = strlen(objects_dir); + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, sizeof(loose_backend), objects_dirlen); + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, 2); + backend = git__calloc(1, alloclen); + GIT_ERROR_CHECK_ALLOC(backend); + + backend->parent.version = GIT_ODB_BACKEND_VERSION; + backend->objects_dirlen = objects_dirlen; + memcpy(backend->objects_dir, objects_dir, objects_dirlen); + + if (backend->objects_dir[backend->objects_dirlen - 1] != '/') + backend->objects_dir[backend->objects_dirlen++] = '/'; + + normalize_options(&backend->options, opts); + backend->oid_hexsize = git_oid_hexsize(backend->options.oid_type); + + backend->parent.read = &loose_backend__read; + backend->parent.write = &loose_backend__write; + backend->parent.read_prefix = &loose_backend__read_prefix; + backend->parent.read_header = &loose_backend__read_header; + backend->parent.writestream = &loose_backend__writestream; + backend->parent.readstream = &loose_backend__readstream; + backend->parent.exists = &loose_backend__exists; + backend->parent.exists_prefix = &loose_backend__exists_prefix; + backend->parent.foreach = &loose_backend__foreach; + backend->parent.freshen = &loose_backend__freshen; + backend->parent.free = &loose_backend__free; + + *backend_out = (git_odb_backend *)backend; + return 0; +} + + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_odb_backend_loose( + git_odb_backend **backend_out, + const char *objects_dir, + git_odb_backend_loose_options *opts) +{ + return git_odb__backend_loose(backend_out, objects_dir, opts); +} +#else +int git_odb_backend_loose( + git_odb_backend **backend_out, + const char *objects_dir, + int compression_level, + int do_fsync, + unsigned int dir_mode, + unsigned int file_mode) +{ + git_odb_backend_loose_flag_t flags = 0; + git_odb_backend_loose_options opts = GIT_ODB_BACKEND_LOOSE_OPTIONS_INIT; + + if (do_fsync) + flags |= GIT_ODB_BACKEND_LOOSE_FSYNC; + + opts.flags = flags; + opts.compression_level = compression_level; + opts.dir_mode = dir_mode; + opts.file_mode = file_mode; + opts.oid_type = GIT_OID_DEFAULT; + + return git_odb__backend_loose(backend_out, objects_dir, &opts); +} +#endif diff --git a/src/libgit2/odb_mempack.c b/src/libgit2/odb_mempack.c new file mode 100644 index 0000000..6f27f45 --- /dev/null +++ b/src/libgit2/odb_mempack.c @@ -0,0 +1,189 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "buf.h" +#include "futils.h" +#include "hash.h" +#include "odb.h" +#include "array.h" +#include "oidmap.h" +#include "pack-objects.h" + +#include "git2/odb_backend.h" +#include "git2/object.h" +#include "git2/types.h" +#include "git2/pack.h" +#include "git2/sys/odb_backend.h" +#include "git2/sys/mempack.h" + +struct memobject { + git_oid oid; + size_t len; + git_object_t type; + char data[GIT_FLEX_ARRAY]; +}; + +struct memory_packer_db { + git_odb_backend parent; + git_oidmap *objects; + git_array_t(struct memobject *) commits; +}; + +static int impl__write(git_odb_backend *_backend, const git_oid *oid, const void *data, size_t len, git_object_t type) +{ + struct memory_packer_db *db = (struct memory_packer_db *)_backend; + struct memobject *obj = NULL; + size_t alloc_len; + + if (git_oidmap_exists(db->objects, oid)) + return 0; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, sizeof(struct memobject), len); + obj = git__malloc(alloc_len); + GIT_ERROR_CHECK_ALLOC(obj); + + memcpy(obj->data, data, len); + git_oid_cpy(&obj->oid, oid); + obj->len = len; + obj->type = type; + + if (git_oidmap_set(db->objects, &obj->oid, obj) < 0) + return -1; + + if (type == GIT_OBJECT_COMMIT) { + struct memobject **store = git_array_alloc(db->commits); + GIT_ERROR_CHECK_ALLOC(store); + *store = obj; + } + + return 0; +} + +static int impl__exists(git_odb_backend *backend, const git_oid *oid) +{ + struct memory_packer_db *db = (struct memory_packer_db *)backend; + + return git_oidmap_exists(db->objects, oid); +} + +static int impl__read(void **buffer_p, size_t *len_p, git_object_t *type_p, git_odb_backend *backend, const git_oid *oid) +{ + struct memory_packer_db *db = (struct memory_packer_db *)backend; + struct memobject *obj; + + if ((obj = git_oidmap_get(db->objects, oid)) == NULL) + return GIT_ENOTFOUND; + + *len_p = obj->len; + *type_p = obj->type; + *buffer_p = git__malloc(obj->len); + GIT_ERROR_CHECK_ALLOC(*buffer_p); + + memcpy(*buffer_p, obj->data, obj->len); + return 0; +} + +static int impl__read_header(size_t *len_p, git_object_t *type_p, git_odb_backend *backend, const git_oid *oid) +{ + struct memory_packer_db *db = (struct memory_packer_db *)backend; + struct memobject *obj; + + if ((obj = git_oidmap_get(db->objects, oid)) == NULL) + return GIT_ENOTFOUND; + + *len_p = obj->len; + *type_p = obj->type; + return 0; +} + +static int git_mempack__dump( + git_str *pack, + git_repository *repo, + git_odb_backend *_backend) +{ + struct memory_packer_db *db = (struct memory_packer_db *)_backend; + git_packbuilder *packbuilder; + uint32_t i; + int err = -1; + + if (git_packbuilder_new(&packbuilder, repo) < 0) + return -1; + + git_packbuilder_set_threads(packbuilder, 0); + + for (i = 0; i < db->commits.size; ++i) { + struct memobject *commit = db->commits.ptr[i]; + + err = git_packbuilder_insert_commit(packbuilder, &commit->oid); + if (err < 0) + goto cleanup; + } + + err = git_packbuilder__write_buf(pack, packbuilder); + +cleanup: + git_packbuilder_free(packbuilder); + return err; +} + +int git_mempack_dump( + git_buf *pack, + git_repository *repo, + git_odb_backend *_backend) +{ + GIT_BUF_WRAP_PRIVATE(pack, git_mempack__dump, repo, _backend); +} + +int git_mempack_reset(git_odb_backend *_backend) +{ + struct memory_packer_db *db = (struct memory_packer_db *)_backend; + struct memobject *object = NULL; + + git_oidmap_foreach_value(db->objects, object, { + git__free(object); + }); + + git_array_clear(db->commits); + + git_oidmap_clear(db->objects); + + return 0; +} + +static void impl__free(git_odb_backend *_backend) +{ + struct memory_packer_db *db = (struct memory_packer_db *)_backend; + + git_mempack_reset(_backend); + git_oidmap_free(db->objects); + git__free(db); +} + +int git_mempack_new(git_odb_backend **out) +{ + struct memory_packer_db *db; + + GIT_ASSERT_ARG(out); + + db = git__calloc(1, sizeof(struct memory_packer_db)); + GIT_ERROR_CHECK_ALLOC(db); + + if (git_oidmap_new(&db->objects) < 0) + return -1; + + db->parent.version = GIT_ODB_BACKEND_VERSION; + db->parent.read = &impl__read; + db->parent.write = &impl__write; + db->parent.read_header = &impl__read_header; + db->parent.exists = &impl__exists; + db->parent.free = &impl__free; + + *out = (git_odb_backend *)db; + return 0; +} diff --git a/src/libgit2/odb_pack.c b/src/libgit2/odb_pack.c new file mode 100644 index 0000000..fc533ae --- /dev/null +++ b/src/libgit2/odb_pack.c @@ -0,0 +1,986 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include +#include "git2/repository.h" +#include "git2/indexer.h" +#include "git2/sys/odb_backend.h" +#include "delta.h" +#include "futils.h" +#include "hash.h" +#include "midx.h" +#include "mwindow.h" +#include "odb.h" +#include "pack.h" + +#include "git2/odb_backend.h" + +/* re-freshen pack files no more than every 2 seconds */ +#define FRESHEN_FREQUENCY 2 + +struct pack_backend { + git_odb_backend parent; + git_odb_backend_pack_options opts; + git_midx_file *midx; + git_vector midx_packs; + git_vector packs; + struct git_pack_file *last_found; + char *pack_folder; +}; + +struct pack_writepack { + struct git_odb_writepack parent; + git_indexer *indexer; +}; + +/** + * The wonderful tale of a Packed Object lookup query + * =================================================== + * A riveting and epic story of epicness and ASCII + * art, presented by yours truly, + * Sir Vicent of Marti + * + * + * Chapter 1: Once upon a time... + * Initialization of the Pack Backend + * -------------------------------------------------- + * + * # git_odb_backend_pack + * | Creates the pack backend structure, initializes the + * | callback pointers to our default read() and exist() methods, + * | and tries to find the `pack` folder, if it exists. ODBs without a `pack` + * | folder are ignored altogether. If there is a `pack` folder, it tries to + * | preload all the known packfiles in the ODB. + * | + * |-# pack_backend__refresh + * | The `multi-pack-index` is loaded if it exists and is valid. + * | Then we run a `dirent` callback through every file in the pack folder, + * | even those present in `multi-pack-index`. The unindexed packfiles are + * | then sorted according to a sorting callback. + * | + * |-# refresh_multi_pack_index + * | Detect the presence of the `multi-pack-index` file. If it needs to be + * | refreshed, frees the old copy and tries to load the new one, together + * | with all the packfiles it indexes. If the process fails, fall back to + * | the old behavior, as if the `multi-pack-index` file was not there. + * | + * |-# packfile_load__cb + * | | This callback is called from `dirent` with every single file + * | | inside the pack folder. We find the packs by actually locating + * | | their index (ends in ".idx"). From that index, we verify that + * | | the corresponding packfile exists and is valid, and if so, we + * | | add it to the pack list. + * | | + * | # git_mwindow_get_pack + * | Make sure that there's a packfile to back this index, and store + * | some very basic information regarding the packfile itself, + * | such as the full path, the size, and the modification time. + * | We don't actually open the packfile to check for internal consistency. + * | + * |-# packfile_sort__cb + * Sort all the preloaded packs according to some specific criteria: + * we prioritize the "newer" packs because it's more likely they + * contain the objects we are looking for, and we prioritize local + * packs over remote ones. + * + * + * + * Chapter 2: To be, or not to be... + * A standard packed `exist` query for an OID + * -------------------------------------------------- + * + * # pack_backend__exists / pack_backend__exists_prefix + * | Check if the given oid (or an oid prefix) exists in any of the + * | packs that have been loaded for our ODB. + * | + * |-# pack_entry_find / pack_entry_find_prefix + * | If there is a multi-pack-index present, search the oid in that + * | index first. If it is not found there, iterate through all the unindexed + * | packs that have been preloaded (starting by the pack where the latest + * | object was found) to try to find the OID in one of them. + * | + * |-# git_midx_entry_find + * | Search for the oid in the multi-pack-index. See + * | + * | for specifics on the multi-pack-index format and how do we find + * | entries in it. + * | + * |-# git_pack_entry_find + * | Check the index of an individual unindexed pack to see if the + * | OID can be found. If we can find the offset to that inside of the + * | index, that means the object is contained inside of the packfile and + * | we can stop searching. Before returning, we verify that the + * | packfile behind the index we are searching still exists on disk. + * | + * |-# pack_entry_find_offset + * | Mmap the actual index file to disk if it hasn't been opened + * | yet, and run a binary search through it to find the OID. + * | See + * | for specifics on the Packfile Index format and how do we find + * | entries in it. + * | + * |-# pack_index_open + * | Guess the name of the index based on the full path to the + * | packfile, open it and verify its contents. Only if the index + * | has not been opened already. + * | + * |-# pack_index_check + * Mmap the index file and do a quick run through the header + * to guess the index version (right now we support v1 and v2), + * and to verify that the size of the index makes sense. + * + * + * + * Chapter 3: The neverending story... + * A standard packed `lookup` query for an OID + * -------------------------------------------------- + * + * # pack_backend__read / pack_backend__read_prefix + * | Check if the given oid (or an oid prefix) exists in any of the + * | packs that have been loaded for our ODB. If it does, open the packfile and + * | read from it. + * | + * |-# git_packfile_unpack + * Armed with a packfile and the offset within it, we can finally unpack + * the object pointed at by the oid. This involves mmapping part of + * the `.pack` file, and uncompressing the object within it (if it is + * stored in the undelfitied representation), or finding a base object and + * applying some deltas to its uncompressed representation (if it is stored + * in the deltified representation). See + * + * for specifics on the Packfile format and how do we read from it. + * + */ + + +/*********************************************************** + * + * FORWARD DECLARATIONS + * + ***********************************************************/ + +static int packfile_sort__cb(const void *a_, const void *b_); + +static int packfile_load__cb(void *_data, git_str *path); + +static int packfile_byname_search_cmp(const void *path, const void *pack_entry); + +static int pack_entry_find(struct git_pack_entry *e, + struct pack_backend *backend, const git_oid *oid); + +/* Can find the offset of an object given + * a prefix of an identifier. + * Sets GIT_EAMBIGUOUS if short oid is ambiguous. + * This method assumes that len is between + * GIT_OID_MINPREFIXLEN and the hexsize for the hash type. + */ +static int pack_entry_find_prefix( + struct git_pack_entry *e, + struct pack_backend *backend, + const git_oid *short_oid, + size_t len); + + + +/*********************************************************** + * + * PACK WINDOW MANAGEMENT + * + ***********************************************************/ + +static int packfile_byname_search_cmp(const void *path_, const void *p_) +{ + const git_str *path = (const git_str *)path_; + const struct git_pack_file *p = (const struct git_pack_file *)p_; + + return strncmp(p->pack_name, git_str_cstr(path), git_str_len(path)); +} + +static int packfile_sort__cb(const void *a_, const void *b_) +{ + const struct git_pack_file *a = a_; + const struct git_pack_file *b = b_; + int st; + + /* + * Local packs tend to contain objects specific to our + * variant of the project than remote ones. In addition, + * remote ones could be on a network mounted filesystem. + * Favor local ones for these reasons. + */ + st = a->pack_local - b->pack_local; + if (st) + return -st; + + /* + * Younger packs tend to contain more recent objects, + * and more recent objects tend to get accessed more + * often. + */ + if (a->mtime < b->mtime) + return 1; + else if (a->mtime == b->mtime) + return 0; + + return -1; +} + + +static int packfile_load__cb(void *data, git_str *path) +{ + struct pack_backend *backend = data; + struct git_pack_file *pack; + const char *path_str = git_str_cstr(path); + git_str index_prefix = GIT_STR_INIT; + size_t cmp_len = git_str_len(path); + int error; + + if (cmp_len <= strlen(".idx") || git__suffixcmp(path_str, ".idx") != 0) + return 0; /* not an index */ + + cmp_len -= strlen(".idx"); + git_str_attach_notowned(&index_prefix, path_str, cmp_len); + + if (git_vector_search2(NULL, &backend->midx_packs, packfile_byname_search_cmp, &index_prefix) == 0) + return 0; + if (git_vector_search2(NULL, &backend->packs, packfile_byname_search_cmp, &index_prefix) == 0) + return 0; + + error = git_mwindow_get_pack(&pack, path->ptr, backend->opts.oid_type); + + /* ignore missing .pack file as git does */ + if (error == GIT_ENOTFOUND) { + git_error_clear(); + return 0; + } + + if (!error) + error = git_vector_insert(&backend->packs, pack); + + return error; + +} + +static int pack_entry_find(struct git_pack_entry *e, struct pack_backend *backend, const git_oid *oid) +{ + struct git_pack_file *last_found = backend->last_found, *p; + git_midx_entry midx_entry; + size_t oid_hexsize = git_oid_hexsize(backend->opts.oid_type); + size_t i; + + if (backend->midx && + git_midx_entry_find(&midx_entry, backend->midx, oid, oid_hexsize) == 0 && + midx_entry.pack_index < git_vector_length(&backend->midx_packs)) { + e->offset = midx_entry.offset; + git_oid_cpy(&e->id, &midx_entry.sha1); + e->p = git_vector_get(&backend->midx_packs, midx_entry.pack_index); + return 0; + } + + if (last_found && + git_pack_entry_find(e, last_found, oid, oid_hexsize) == 0) + return 0; + + git_vector_foreach(&backend->packs, i, p) { + if (p == last_found) + continue; + + if (git_pack_entry_find(e, p, oid, oid_hexsize) == 0) { + backend->last_found = p; + return 0; + } + } + + return git_odb__error_notfound( + "failed to find pack entry", oid, oid_hexsize); +} + +static int pack_entry_find_prefix( + struct git_pack_entry *e, + struct pack_backend *backend, + const git_oid *short_oid, + size_t len) +{ + int error; + size_t i; + git_oid found_full_oid; + bool found = false; + struct git_pack_file *last_found = backend->last_found, *p; + git_midx_entry midx_entry; + +#ifdef GIT_EXPERIMENTAL_SHA256 + git_oid_clear(&found_full_oid, short_oid->type); +#else + git_oid_clear(&found_full_oid, GIT_OID_SHA1); +#endif + + if (backend->midx) { + error = git_midx_entry_find(&midx_entry, backend->midx, short_oid, len); + if (error == GIT_EAMBIGUOUS) + return error; + if (!error && midx_entry.pack_index < git_vector_length(&backend->midx_packs)) { + e->offset = midx_entry.offset; + git_oid_cpy(&e->id, &midx_entry.sha1); + e->p = git_vector_get(&backend->midx_packs, midx_entry.pack_index); + git_oid_cpy(&found_full_oid, &e->id); + found = true; + } + } + + if (last_found) { + error = git_pack_entry_find(e, last_found, short_oid, len); + if (error == GIT_EAMBIGUOUS) + return error; + if (!error) { + if (found && git_oid_cmp(&e->id, &found_full_oid)) + return git_odb__error_ambiguous("found multiple pack entries"); + git_oid_cpy(&found_full_oid, &e->id); + found = true; + } + } + + git_vector_foreach(&backend->packs, i, p) { + if (p == last_found) + continue; + + error = git_pack_entry_find(e, p, short_oid, len); + if (error == GIT_EAMBIGUOUS) + return error; + if (!error) { + if (found && git_oid_cmp(&e->id, &found_full_oid)) + return git_odb__error_ambiguous("found multiple pack entries"); + git_oid_cpy(&found_full_oid, &e->id); + found = true; + backend->last_found = p; + } + } + + if (!found) + return git_odb__error_notfound("no matching pack entry for prefix", + short_oid, len); + else + return 0; +} + +/*********************************************************** + * + * MULTI-PACK-INDEX SUPPORT + * + * Functions needed to support the multi-pack-index. + * + ***********************************************************/ + +/* + * Remove the multi-pack-index, and move all midx_packs to packs. + */ +static int remove_multi_pack_index(struct pack_backend *backend) +{ + size_t i, j = git_vector_length(&backend->packs); + struct pack_backend *p; + int error = git_vector_size_hint( + &backend->packs, + j + git_vector_length(&backend->midx_packs)); + if (error < 0) + return error; + + git_vector_foreach(&backend->midx_packs, i, p) + git_vector_set(NULL, &backend->packs, j++, p); + git_vector_clear(&backend->midx_packs); + + git_midx_free(backend->midx); + backend->midx = NULL; + + return 0; +} + +/* + * Loads a single .pack file referred to by the multi-pack-index. These must + * match the order in which they are declared in the multi-pack-index file, + * since these files are referred to by their index. + */ +static int process_multi_pack_index_pack( + struct pack_backend *backend, + size_t i, + const char *packfile_name) +{ + int error; + struct git_pack_file *pack; + size_t found_position; + git_str pack_path = GIT_STR_INIT, index_prefix = GIT_STR_INIT; + + error = git_str_joinpath(&pack_path, backend->pack_folder, packfile_name); + if (error < 0) + return error; + + /* This is ensured by midx_parse_packfile_name() */ + if (git_str_len(&pack_path) <= strlen(".idx") || git__suffixcmp(git_str_cstr(&pack_path), ".idx") != 0) + return git_odb__error_notfound("midx file contained a non-index", NULL, 0); + + git_str_attach_notowned(&index_prefix, git_str_cstr(&pack_path), git_str_len(&pack_path) - strlen(".idx")); + + if (git_vector_search2(&found_position, &backend->packs, packfile_byname_search_cmp, &index_prefix) == 0) { + /* Pack was found in the packs list. Moving it to the midx_packs list. */ + git_str_dispose(&pack_path); + git_vector_set(NULL, &backend->midx_packs, i, git_vector_get(&backend->packs, found_position)); + git_vector_remove(&backend->packs, found_position); + return 0; + } + + /* Pack was not found. Allocate a new one. */ + error = git_mwindow_get_pack( + &pack, + git_str_cstr(&pack_path), + backend->opts.oid_type); + git_str_dispose(&pack_path); + if (error < 0) + return error; + + git_vector_set(NULL, &backend->midx_packs, i, pack); + return 0; +} + +/* + * Reads the multi-pack-index. If this fails for whatever reason, the + * multi-pack-index object is freed, and all the packfiles that are related to + * it are moved to the unindexed packfiles vector. + */ +static int refresh_multi_pack_index(struct pack_backend *backend) +{ + int error; + git_str midx_path = GIT_STR_INIT; + const char *packfile_name; + size_t i; + + error = git_str_joinpath(&midx_path, backend->pack_folder, "multi-pack-index"); + if (error < 0) + return error; + + /* + * Check whether the multi-pack-index has changed. If it has, close any + * old multi-pack-index and move all the packfiles to the unindexed + * packs. This is done to prevent losing any open packfiles in case + * refreshing the new multi-pack-index fails, or the file is deleted. + */ + if (backend->midx) { + if (!git_midx_needs_refresh(backend->midx, git_str_cstr(&midx_path))) { + git_str_dispose(&midx_path); + return 0; + } + error = remove_multi_pack_index(backend); + if (error < 0) { + git_str_dispose(&midx_path); + return error; + } + } + + error = git_midx_open(&backend->midx, git_str_cstr(&midx_path), + backend->opts.oid_type); + + git_str_dispose(&midx_path); + if (error < 0) + return error; + + git_vector_resize_to(&backend->midx_packs, git_vector_length(&backend->midx->packfile_names)); + + git_vector_foreach(&backend->midx->packfile_names, i, packfile_name) { + error = process_multi_pack_index_pack(backend, i, packfile_name); + if (error < 0) { + /* + * Something failed during reading multi-pack-index. + * Restore the state of backend as if the + * multi-pack-index was never there, and move all + * packfiles that have been processed so far to the + * unindexed packs. + */ + git_vector_resize_to(&backend->midx_packs, i); + remove_multi_pack_index(backend); + return error; + } + } + + return 0; +} + +/*********************************************************** + * + * PACKED BACKEND PUBLIC API + * + * Implement the git_odb_backend API calls + * + ***********************************************************/ +static int pack_backend__refresh(git_odb_backend *backend_) +{ + int error; + struct stat st; + git_str path = GIT_STR_INIT; + struct pack_backend *backend = (struct pack_backend *)backend_; + + if (backend->pack_folder == NULL) + return 0; + + if (p_stat(backend->pack_folder, &st) < 0 || !S_ISDIR(st.st_mode)) + return git_odb__error_notfound("failed to refresh packfiles", NULL, 0); + + if (refresh_multi_pack_index(backend) < 0) { + /* + * It is okay if this fails. We will just not use the + * multi-pack-index in this case. + */ + git_error_clear(); + } + + /* reload all packs */ + git_str_sets(&path, backend->pack_folder); + error = git_fs_path_direach(&path, 0, packfile_load__cb, backend); + + git_str_dispose(&path); + git_vector_sort(&backend->packs); + + return error; +} + +static int pack_backend__read_header( + size_t *len_p, git_object_t *type_p, + struct git_odb_backend *backend, const git_oid *oid) +{ + struct git_pack_entry e; + int error; + + GIT_ASSERT_ARG(len_p); + GIT_ASSERT_ARG(type_p); + GIT_ASSERT_ARG(backend); + GIT_ASSERT_ARG(oid); + + if ((error = pack_entry_find(&e, (struct pack_backend *)backend, oid)) < 0) + return error; + + return git_packfile_resolve_header(len_p, type_p, e.p, e.offset); +} + +static int pack_backend__freshen( + git_odb_backend *backend, const git_oid *oid) +{ + struct git_pack_entry e; + time_t now; + int error; + + if ((error = pack_entry_find(&e, (struct pack_backend *)backend, oid)) < 0) + return error; + + now = time(NULL); + + if (e.p->last_freshen > now - FRESHEN_FREQUENCY) + return 0; + + if ((error = git_futils_touch(e.p->pack_name, &now)) < 0) + return error; + + e.p->last_freshen = now; + return 0; +} + +static int pack_backend__read( + void **buffer_p, size_t *len_p, git_object_t *type_p, + git_odb_backend *backend, const git_oid *oid) +{ + struct git_pack_entry e; + git_rawobj raw = {NULL}; + int error; + + if ((error = pack_entry_find(&e, (struct pack_backend *)backend, oid)) < 0 || + (error = git_packfile_unpack(&raw, e.p, &e.offset)) < 0) + return error; + + *buffer_p = raw.data; + *len_p = raw.len; + *type_p = raw.type; + + return 0; +} + +static int pack_backend__read_prefix( + git_oid *out_oid, + void **buffer_p, + size_t *len_p, + git_object_t *type_p, + git_odb_backend *_backend, + const git_oid *short_oid, + size_t len) +{ + struct pack_backend *backend = (struct pack_backend *)_backend; + int error = 0; + + if (len < GIT_OID_MINPREFIXLEN) + error = git_odb__error_ambiguous("prefix length too short"); + + else if (len >= git_oid_hexsize(backend->opts.oid_type)) { + /* We can fall back to regular read method */ + error = pack_backend__read(buffer_p, len_p, type_p, _backend, short_oid); + if (!error) + git_oid_cpy(out_oid, short_oid); + } else { + struct git_pack_entry e; + git_rawobj raw = {NULL}; + + if ((error = pack_entry_find_prefix(&e, + backend, short_oid, len)) == 0 && + (error = git_packfile_unpack(&raw, e.p, &e.offset)) == 0) + { + *buffer_p = raw.data; + *len_p = raw.len; + *type_p = raw.type; + git_oid_cpy(out_oid, &e.id); + } + } + + return error; +} + +static int pack_backend__exists(git_odb_backend *backend, const git_oid *oid) +{ + struct git_pack_entry e; + return pack_entry_find(&e, (struct pack_backend *)backend, oid) == 0; +} + +static int pack_backend__exists_prefix( + git_oid *out, git_odb_backend *backend, const git_oid *short_id, size_t len) +{ + int error; + struct pack_backend *pb = (struct pack_backend *)backend; + struct git_pack_entry e = {0}; + + error = pack_entry_find_prefix(&e, pb, short_id, len); + git_oid_cpy(out, &e.id); + return error; +} + +static int pack_backend__foreach(git_odb_backend *_backend, git_odb_foreach_cb cb, void *data) +{ + int error; + struct git_pack_file *p; + struct pack_backend *backend; + unsigned int i; + + GIT_ASSERT_ARG(_backend); + GIT_ASSERT_ARG(cb); + + backend = (struct pack_backend *)_backend; + + /* Make sure we know about the packfiles */ + if ((error = pack_backend__refresh(_backend)) != 0) + return error; + + if (backend->midx && (error = git_midx_foreach_entry(backend->midx, cb, data)) != 0) + return error; + git_vector_foreach(&backend->packs, i, p) { + if ((error = git_pack_foreach_entry(p, cb, data)) != 0) + return error; + } + + return 0; +} + +static int pack_backend__writepack_append(struct git_odb_writepack *_writepack, const void *data, size_t size, git_indexer_progress *stats) +{ + struct pack_writepack *writepack = (struct pack_writepack *)_writepack; + + GIT_ASSERT_ARG(writepack); + + return git_indexer_append(writepack->indexer, data, size, stats); +} + +static int pack_backend__writepack_commit(struct git_odb_writepack *_writepack, git_indexer_progress *stats) +{ + struct pack_writepack *writepack = (struct pack_writepack *)_writepack; + + GIT_ASSERT_ARG(writepack); + + return git_indexer_commit(writepack->indexer, stats); +} + +static void pack_backend__writepack_free(struct git_odb_writepack *_writepack) +{ + struct pack_writepack *writepack; + + if (!_writepack) + return; + + writepack = (struct pack_writepack *)_writepack; + + git_indexer_free(writepack->indexer); + git__free(writepack); +} + +static int pack_backend__writepack(struct git_odb_writepack **out, + git_odb_backend *_backend, + git_odb *odb, + git_indexer_progress_cb progress_cb, + void *progress_payload) +{ + git_indexer_options opts = GIT_INDEXER_OPTIONS_INIT; + struct pack_backend *backend; + struct pack_writepack *writepack; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(_backend); + + *out = NULL; + + opts.progress_cb = progress_cb; + opts.progress_cb_payload = progress_payload; + + backend = (struct pack_backend *)_backend; + + writepack = git__calloc(1, sizeof(struct pack_writepack)); + GIT_ERROR_CHECK_ALLOC(writepack); + +#ifdef GIT_EXPERIMENTAL_SHA256 + opts.odb = odb; + + error = git_indexer_new(&writepack->indexer, + backend->pack_folder, + backend->opts.oid_type, + &opts); +#else + error = git_indexer_new(&writepack->indexer, + backend->pack_folder, 0, odb, &opts); +#endif + + if (error < 0) + return -1; + + writepack->parent.backend = _backend; + writepack->parent.append = pack_backend__writepack_append; + writepack->parent.commit = pack_backend__writepack_commit; + writepack->parent.free = pack_backend__writepack_free; + + *out = (git_odb_writepack *)writepack; + + return 0; +} + +static int get_idx_path( + git_str *idx_path, + struct pack_backend *backend, + struct git_pack_file *p) +{ + size_t path_len; + int error; + + error = git_fs_path_prettify(idx_path, p->pack_name, backend->pack_folder); + if (error < 0) + return error; + path_len = git_str_len(idx_path); + if (path_len <= strlen(".pack") || git__suffixcmp(git_str_cstr(idx_path), ".pack") != 0) + return git_odb__error_notfound("packfile does not end in .pack", NULL, 0); + path_len -= strlen(".pack"); + error = git_str_splice(idx_path, path_len, strlen(".pack"), ".idx", strlen(".idx")); + if (error < 0) + return error; + + return 0; +} + +static int pack_backend__writemidx(git_odb_backend *_backend) +{ + struct pack_backend *backend; + git_midx_writer *w = NULL; + struct git_pack_file *p; + size_t i; + int error = 0; + + GIT_ASSERT_ARG(_backend); + + backend = (struct pack_backend *)_backend; + + error = git_midx_writer_new(&w, backend->pack_folder +#ifdef GIT_EXPERIMENTAL_SHA256 + , backend->opts.oid_type +#endif + ); + + if (error < 0) + return error; + + git_vector_foreach(&backend->midx_packs, i, p) { + git_str idx_path = GIT_STR_INIT; + error = get_idx_path(&idx_path, backend, p); + if (error < 0) + goto cleanup; + error = git_midx_writer_add(w, git_str_cstr(&idx_path)); + git_str_dispose(&idx_path); + if (error < 0) + goto cleanup; + } + git_vector_foreach(&backend->packs, i, p) { + git_str idx_path = GIT_STR_INIT; + error = get_idx_path(&idx_path, backend, p); + if (error < 0) + goto cleanup; + error = git_midx_writer_add(w, git_str_cstr(&idx_path)); + git_str_dispose(&idx_path); + if (error < 0) + goto cleanup; + } + + /* + * Invalidate the previous midx before writing the new one. + */ + error = remove_multi_pack_index(backend); + if (error < 0) + goto cleanup; + error = git_midx_writer_commit(w); + if (error < 0) + goto cleanup; + error = refresh_multi_pack_index(backend); + +cleanup: + git_midx_writer_free(w); + return error; +} + +static void pack_backend__free(git_odb_backend *_backend) +{ + struct pack_backend *backend; + struct git_pack_file *p; + size_t i; + + if (!_backend) + return; + + backend = (struct pack_backend *)_backend; + + git_vector_foreach(&backend->midx_packs, i, p) + git_mwindow_put_pack(p); + git_vector_foreach(&backend->packs, i, p) + git_mwindow_put_pack(p); + + git_midx_free(backend->midx); + git_vector_free(&backend->midx_packs); + git_vector_free(&backend->packs); + git__free(backend->pack_folder); + git__free(backend); +} + +static int pack_backend__alloc( + struct pack_backend **out, + size_t initial_size, + const git_odb_backend_pack_options *opts) +{ + struct pack_backend *backend = git__calloc(1, sizeof(struct pack_backend)); + GIT_ERROR_CHECK_ALLOC(backend); + + if (git_vector_init(&backend->midx_packs, 0, NULL) < 0) { + git__free(backend); + return -1; + } + + if (git_vector_init(&backend->packs, initial_size, packfile_sort__cb) < 0) { + git_vector_free(&backend->midx_packs); + git__free(backend); + return -1; + } + + if (opts) + memcpy(&backend->opts, opts, sizeof(git_odb_backend_pack_options)); + + if (!backend->opts.oid_type) + backend->opts.oid_type = GIT_OID_DEFAULT; + + backend->parent.version = GIT_ODB_BACKEND_VERSION; + + backend->parent.read = &pack_backend__read; + backend->parent.read_prefix = &pack_backend__read_prefix; + backend->parent.read_header = &pack_backend__read_header; + backend->parent.exists = &pack_backend__exists; + backend->parent.exists_prefix = &pack_backend__exists_prefix; + backend->parent.refresh = &pack_backend__refresh; + backend->parent.foreach = &pack_backend__foreach; + backend->parent.writepack = &pack_backend__writepack; + backend->parent.writemidx = &pack_backend__writemidx; + backend->parent.freshen = &pack_backend__freshen; + backend->parent.free = &pack_backend__free; + + *out = backend; + return 0; +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_odb_backend_one_pack( + git_odb_backend **backend_out, + const char *idx, + const git_odb_backend_pack_options *opts) +#else +int git_odb_backend_one_pack( + git_odb_backend **backend_out, + const char *idx) +#endif +{ + struct pack_backend *backend = NULL; + struct git_pack_file *packfile = NULL; + +#ifndef GIT_EXPERIMENTAL_SHA256 + git_odb_backend_pack_options *opts = NULL; +#endif + + git_oid_t oid_type = opts ? opts->oid_type : 0; + + if (pack_backend__alloc(&backend, 1, opts) < 0) + return -1; + + if (git_mwindow_get_pack(&packfile, idx, oid_type) < 0 || + git_vector_insert(&backend->packs, packfile) < 0) { + pack_backend__free((git_odb_backend *)backend); + return -1; + } + + *backend_out = (git_odb_backend *)backend; + return 0; +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_odb_backend_pack( + git_odb_backend **backend_out, + const char *objects_dir, + const git_odb_backend_pack_options *opts) +#else +int git_odb_backend_pack( + git_odb_backend **backend_out, + const char *objects_dir) +#endif +{ + int error = 0; + struct pack_backend *backend = NULL; + git_str path = GIT_STR_INIT; + +#ifndef GIT_EXPERIMENTAL_SHA256 + git_odb_backend_pack_options *opts = NULL; +#endif + + if (pack_backend__alloc(&backend, 8, opts) < 0) + return -1; + + if (!(error = git_str_joinpath(&path, objects_dir, "pack")) && + git_fs_path_isdir(git_str_cstr(&path))) { + backend->pack_folder = git_str_detach(&path); + error = pack_backend__refresh((git_odb_backend *)backend); + } + + if (error < 0) { + pack_backend__free((git_odb_backend *)backend); + backend = NULL; + } + + *backend_out = (git_odb_backend *)backend; + + git_str_dispose(&path); + + return error; +} diff --git a/src/libgit2/offmap.c b/src/libgit2/offmap.c new file mode 100644 index 0000000..be9eb66 --- /dev/null +++ b/src/libgit2/offmap.c @@ -0,0 +1,101 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "offmap.h" + +#define kmalloc git__malloc +#define kcalloc git__calloc +#define krealloc git__realloc +#define kreallocarray git__reallocarray +#define kfree git__free +#include "khash.h" + +__KHASH_TYPE(off, off64_t, void *) + +__KHASH_IMPL(off, static kh_inline, off64_t, void *, 1, kh_int64_hash_func, kh_int64_hash_equal) + + +int git_offmap_new(git_offmap **out) +{ + *out = kh_init(off); + GIT_ERROR_CHECK_ALLOC(*out); + + return 0; +} + +void git_offmap_free(git_offmap *map) +{ + kh_destroy(off, map); +} + +void git_offmap_clear(git_offmap *map) +{ + kh_clear(off, map); +} + +size_t git_offmap_size(git_offmap *map) +{ + return kh_size(map); +} + +void *git_offmap_get(git_offmap *map, const off64_t key) +{ + size_t idx = kh_get(off, map, key); + if (idx == kh_end(map) || !kh_exist(map, idx)) + return NULL; + return kh_val(map, idx); +} + +int git_offmap_set(git_offmap *map, const off64_t key, void *value) +{ + size_t idx; + int rval; + + idx = kh_put(off, map, key, &rval); + if (rval < 0) + return -1; + + if (rval == 0) + kh_key(map, idx) = key; + + kh_val(map, idx) = value; + + return 0; +} + +int git_offmap_delete(git_offmap *map, const off64_t key) +{ + khiter_t idx = kh_get(off, map, key); + if (idx == kh_end(map)) + return GIT_ENOTFOUND; + kh_del(off, map, idx); + return 0; +} + +int git_offmap_exists(git_offmap *map, const off64_t key) +{ + return kh_get(off, map, key) != kh_end(map); +} + +int git_offmap_iterate(void **value, git_offmap *map, size_t *iter, off64_t *key) +{ + size_t i = *iter; + + while (i < map->n_buckets && !kh_exist(map, i)) + i++; + + if (i >= map->n_buckets) + return GIT_ITEROVER; + + if (key) + *key = kh_key(map, i); + if (value) + *value = kh_value(map, i); + *iter = ++i; + + return 0; +} diff --git a/src/libgit2/offmap.h b/src/libgit2/offmap.h new file mode 100644 index 0000000..81c459b --- /dev/null +++ b/src/libgit2/offmap.h @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2012 the libgit2 contributors + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_offmap_h__ +#define INCLUDE_offmap_h__ + +#include "common.h" + +#include "git2/types.h" + +/** A map with `off64_t`s as key. */ +typedef struct kh_off_s git_offmap; + +/** + * Allocate a new `off64_t` map. + * + * @param out Pointer to the map that shall be allocated. + * @return 0 on success, an error code if allocation has failed. + */ +int git_offmap_new(git_offmap **out); + +/** + * Free memory associated with the map. + * + * Note that this function will _not_ free values added to this + * map. + * + * @param map Pointer to the map that is to be free'd. May be + * `NULL`. + */ +void git_offmap_free(git_offmap *map); + +/** + * Clear all entries from the map. + * + * This function will remove all entries from the associated map. + * Memory associated with it will not be released, though. + * + * @param map Pointer to the map that shall be cleared. May be + * `NULL`. + */ +void git_offmap_clear(git_offmap *map); + +/** + * Return the number of elements in the map. + * + * @parameter map map containing the elements + * @return number of elements in the map + */ +size_t git_offmap_size(git_offmap *map); + +/** + * Return value associated with the given key. + * + * @param map map to search key in + * @param key key to search for + * @return value associated with the given key or NULL if the key was not found + */ +void *git_offmap_get(git_offmap *map, const off64_t key); + +/** + * Set the entry for key to value. + * + * If the map has no corresponding entry for the given key, a new + * entry will be created with the given value. If an entry exists + * already, its value will be updated to match the given value. + * + * @param map map to create new entry in + * @param key key to set + * @param value value to associate the key with; may be NULL + * @return zero if the key was successfully set, a negative error + * code otherwise + */ +int git_offmap_set(git_offmap *map, const off64_t key, void *value); + +/** + * Delete an entry from the map. + * + * Delete the given key and its value from the map. If no such + * key exists, this will do nothing. + * + * @param map map to delete key in + * @param key key to delete + * @return `0` if the key has been deleted, GIT_ENOTFOUND if no + * such key was found, a negative code in case of an + * error + */ +int git_offmap_delete(git_offmap *map, const off64_t key); + +/** + * Check whether a key exists in the given map. + * + * @param map map to query for the key + * @param key key to search for + * @return 0 if the key has not been found, 1 otherwise + */ +int git_offmap_exists(git_offmap *map, const off64_t key); + +/** + * Iterate over entries of the map. + * + * This functions allows to iterate over all key-value entries of + * the map. The current position is stored in the `iter` variable + * and should be initialized to `0` before the first call to this + * function. + * + * @param map map to iterate over + * @param value pointer to the variable where to store the current + * value. May be NULL. + * @param iter iterator storing the current position. Initialize + * with zero previous to the first call. + * @param key pointer to the variable where to store the current + * key. May be NULL. + * @return `0` if the next entry was correctly retrieved. + * GIT_ITEROVER if no entries are left. A negative error + * code otherwise. + */ +int git_offmap_iterate(void **value, git_offmap *map, size_t *iter, off64_t *key); + +#define git_offmap_foreach(h, kvar, vvar, code) { size_t __i = 0; \ + while (git_offmap_iterate((void **) &(vvar), h, &__i, &(kvar)) == 0) { \ + code; \ + } } + +#define git_offmap_foreach_value(h, vvar, code) { size_t __i = 0; \ + while (git_offmap_iterate((void **) &(vvar), h, &__i, NULL) == 0) { \ + code; \ + } } + +#endif diff --git a/src/libgit2/oid.c b/src/libgit2/oid.c new file mode 100644 index 0000000..631a566 --- /dev/null +++ b/src/libgit2/oid.c @@ -0,0 +1,508 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "oid.h" + +#include "git2/oid.h" +#include "repository.h" +#include "threadstate.h" +#include +#include + +const git_oid git_oid__empty_blob_sha1 = + GIT_OID_INIT(GIT_OID_SHA1, + { 0xe6, 0x9d, 0xe2, 0x9b, 0xb2, 0xd1, 0xd6, 0x43, 0x4b, 0x8b, + 0x29, 0xae, 0x77, 0x5a, 0xd8, 0xc2, 0xe4, 0x8c, 0x53, 0x91 }); +const git_oid git_oid__empty_tree_sha1 = + GIT_OID_INIT(GIT_OID_SHA1, + { 0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, + 0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88, 0xfb, 0xee, 0x49, 0x04 }); + +static int oid_error_invalid(const char *msg) +{ + git_error_set(GIT_ERROR_INVALID, "unable to parse OID - %s", msg); + return -1; +} + +int git_oid__fromstrn( + git_oid *out, + const char *str, + size_t length, + git_oid_t type) +{ + size_t size, p; + int v; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(str); + + if (!(size = git_oid_size(type))) + return oid_error_invalid("unknown type"); + + if (!length) + return oid_error_invalid("too short"); + + if (length > git_oid_hexsize(type)) + return oid_error_invalid("too long"); + +#ifdef GIT_EXPERIMENTAL_SHA256 + out->type = type; +#endif + memset(out->id, 0, size); + + for (p = 0; p < length; p++) { + v = git__fromhex(str[p]); + if (v < 0) + return oid_error_invalid("contains invalid characters"); + + out->id[p / 2] |= (unsigned char)(v << (p % 2 ? 0 : 4)); + } + + return 0; +} + +int git_oid__fromstrp(git_oid *out, const char *str, git_oid_t type) +{ + return git_oid__fromstrn(out, str, strlen(str), type); +} + +int git_oid__fromstr(git_oid *out, const char *str, git_oid_t type) +{ + return git_oid__fromstrn(out, str, git_oid_hexsize(type), type); +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_oid_fromstrn( + git_oid *out, + const char *str, + size_t length, + git_oid_t type) +{ + return git_oid__fromstrn(out, str, length, type); +} + +int git_oid_fromstrp(git_oid *out, const char *str, git_oid_t type) +{ + return git_oid_fromstrn(out, str, strlen(str), type); +} + +int git_oid_fromstr(git_oid *out, const char *str, git_oid_t type) +{ + return git_oid_fromstrn(out, str, git_oid_hexsize(type), type); +} +#else +int git_oid_fromstrn( + git_oid *out, + const char *str, + size_t length) +{ + return git_oid__fromstrn(out, str, length, GIT_OID_SHA1); +} + +int git_oid_fromstrp(git_oid *out, const char *str) +{ + return git_oid__fromstrn(out, str, strlen(str), GIT_OID_SHA1); +} + +int git_oid_fromstr(git_oid *out, const char *str) +{ + return git_oid__fromstrn(out, str, GIT_OID_SHA1_HEXSIZE, GIT_OID_SHA1); +} +#endif + +int git_oid_nfmt(char *str, size_t n, const git_oid *oid) +{ + size_t hex_size; + + if (!oid) { + memset(str, 0, n); + return 0; + } + + if (!(hex_size = git_oid_hexsize(git_oid_type(oid)))) + return oid_error_invalid("unknown type"); + + if (n > hex_size) { + memset(&str[hex_size], 0, n - hex_size); + n = hex_size; + } + + git_oid_fmt_substr(str, oid, 0, n); + return 0; +} + +int git_oid_fmt(char *str, const git_oid *oid) +{ + return git_oid_nfmt(str, git_oid_hexsize(git_oid_type(oid)), oid); +} + +int git_oid_pathfmt(char *str, const git_oid *oid) +{ + size_t hex_size; + + if (!(hex_size = git_oid_hexsize(git_oid_type(oid)))) + return oid_error_invalid("unknown type"); + + git_oid_fmt_substr(str, oid, 0, 2); + str[2] = '/'; + git_oid_fmt_substr(&str[3], oid, 2, (hex_size - 2)); + return 0; +} + +char *git_oid_tostr_s(const git_oid *oid) +{ + git_threadstate *threadstate = git_threadstate_get(); + char *str; + + if (!threadstate) + return NULL; + + str = threadstate->oid_fmt; + git_oid_nfmt(str, git_oid_hexsize(git_oid_type(oid)) + 1, oid); + return str; +} + +char *git_oid_allocfmt(const git_oid *oid) +{ + size_t hex_size = git_oid_hexsize(git_oid_type(oid)); + char *str = git__malloc(hex_size + 1); + + if (!hex_size || !str) + return NULL; + + if (git_oid_nfmt(str, hex_size + 1, oid) < 0) { + git__free(str); + return NULL; + } + + return str; +} + +char *git_oid_tostr(char *out, size_t n, const git_oid *oid) +{ + size_t hex_size; + + if (!out || n == 0) + return ""; + + hex_size = oid ? git_oid_hexsize(git_oid_type(oid)) : 0; + + if (n > hex_size + 1) + n = hex_size + 1; + + git_oid_nfmt(out, n - 1, oid); /* allow room for terminating NUL */ + out[n - 1] = '\0'; + + return out; +} + +int git_oid__fromraw(git_oid *out, const unsigned char *raw, git_oid_t type) +{ + size_t size; + + if (!(size = git_oid_size(type))) + return oid_error_invalid("unknown type"); + +#ifdef GIT_EXPERIMENTAL_SHA256 + out->type = type; +#endif + memcpy(out->id, raw, size); + return 0; +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_oid_fromraw(git_oid *out, const unsigned char *raw, git_oid_t type) +{ + return git_oid__fromraw(out, raw, type); +} +#else +int git_oid_fromraw(git_oid *out, const unsigned char *raw) +{ + return git_oid__fromraw(out, raw, GIT_OID_SHA1); +} +#endif + +int git_oid_cpy(git_oid *out, const git_oid *src) +{ + size_t size; + + if (!(size = git_oid_size(git_oid_type(src)))) + return oid_error_invalid("unknown type"); + +#ifdef GIT_EXPERIMENTAL_SHA256 + out->type = src->type; +#endif + + return git_oid_raw_cpy(out->id, src->id, size); +} + +int git_oid_cmp(const git_oid *a, const git_oid *b) +{ + return git_oid__cmp(a, b); +} + +int git_oid_equal(const git_oid *a, const git_oid *b) +{ + return (git_oid__cmp(a, b) == 0); +} + +int git_oid_ncmp(const git_oid *oid_a, const git_oid *oid_b, size_t len) +{ +#ifdef GIT_EXPERIMENTAL_SHA256 + if (oid_a->type != oid_b->type) + return oid_a->type - oid_b->type; +#endif + + return git_oid_raw_ncmp(oid_a->id, oid_b->id, len); +} + +int git_oid_strcmp(const git_oid *oid_a, const char *str) +{ + const unsigned char *a; + unsigned char strval; + long size = (long)git_oid_size(git_oid_type(oid_a)); + int hexval; + + for (a = oid_a->id; *str && (a - oid_a->id) < size; ++a) { + if ((hexval = git__fromhex(*str++)) < 0) + return -1; + strval = (unsigned char)(hexval << 4); + if (*str) { + if ((hexval = git__fromhex(*str++)) < 0) + return -1; + strval |= hexval; + } + if (*a != strval) + return (*a - strval); + } + + return 0; +} + +int git_oid_streq(const git_oid *oid_a, const char *str) +{ + return git_oid_strcmp(oid_a, str) == 0 ? 0 : -1; +} + +int git_oid_is_zero(const git_oid *oid_a) +{ + const unsigned char *a = oid_a->id; + size_t size = git_oid_size(git_oid_type(oid_a)), i; + +#ifdef GIT_EXPERIMENTAL_SHA256 + if (!oid_a->type) + return 1; + else if (!size) + return 0; +#endif + + for (i = 0; i < size; ++i, ++a) + if (*a != 0) + return 0; + return 1; +} + +#ifndef GIT_DEPRECATE_HARD +int git_oid_iszero(const git_oid *oid_a) +{ + return git_oid_is_zero(oid_a); +} +#endif + +typedef short node_index; + +typedef union { + const char *tail; + node_index children[16]; +} trie_node; + +struct git_oid_shorten { + trie_node *nodes; + size_t node_count, size; + int min_length, full; +}; + +static int resize_trie(git_oid_shorten *self, size_t new_size) +{ + self->nodes = git__reallocarray(self->nodes, new_size, sizeof(trie_node)); + GIT_ERROR_CHECK_ALLOC(self->nodes); + + if (new_size > self->size) { + memset(&self->nodes[self->size], 0x0, (new_size - self->size) * sizeof(trie_node)); + } + + self->size = new_size; + return 0; +} + +static trie_node *push_leaf(git_oid_shorten *os, node_index idx, int push_at, const char *oid) +{ + trie_node *node, *leaf; + node_index idx_leaf; + + if (os->node_count >= os->size) { + if (resize_trie(os, os->size * 2) < 0) + return NULL; + } + + idx_leaf = (node_index)os->node_count++; + + if (os->node_count == SHRT_MAX) { + os->full = 1; + return NULL; + } + + node = &os->nodes[idx]; + node->children[push_at] = -idx_leaf; + + leaf = &os->nodes[idx_leaf]; + leaf->tail = oid; + + return node; +} + +git_oid_shorten *git_oid_shorten_new(size_t min_length) +{ + git_oid_shorten *os; + + GIT_ASSERT_ARG_WITH_RETVAL((size_t)((int)min_length) == min_length, NULL); + + os = git__calloc(1, sizeof(git_oid_shorten)); + if (os == NULL) + return NULL; + + if (resize_trie(os, 16) < 0) { + git__free(os); + return NULL; + } + + os->node_count = 1; + os->min_length = (int)min_length; + + return os; +} + +void git_oid_shorten_free(git_oid_shorten *os) +{ + if (os == NULL) + return; + + git__free(os->nodes); + git__free(os); +} + + +/* + * What wizardry is this? + * + * This is just a memory-optimized trie: basically a very fancy + * 16-ary tree, which is used to store the prefixes of the OID + * strings. + * + * Read more: http://en.wikipedia.org/wiki/Trie + * + * Magic that happens in this method: + * + * - Each node in the trie is an union, so it can work both as + * a normal node, or as a leaf. + * + * - Each normal node points to 16 children (one for each possible + * character in the oid). This is *not* stored in an array of + * pointers, because in a 64-bit arch this would be sucking + * 16*sizeof(void*) = 128 bytes of memory per node, which is + * insane. What we do is store Node Indexes, and use these indexes + * to look up each node in the om->index array. These indexes are + * signed shorts, so this limits the amount of unique OIDs that + * fit in the structure to about 20000 (assuming a more or less uniform + * distribution). + * + * - All the nodes in om->index array are stored contiguously in + * memory, and each of them is 32 bytes, so we fit 2x nodes per + * cache line. Convenient for speed. + * + * - To differentiate the leafs from the normal nodes, we store all + * the indexes towards a leaf as a negative index (indexes to normal + * nodes are positives). When we find that one of the children for + * a node has a negative value, that means it's going to be a leaf. + * This reduces the amount of indexes we have by two, but also reduces + * the size of each node by 1-4 bytes (the amount we would need to + * add a `is_leaf` field): this is good because it allows the nodes + * to fit cleanly in cache lines. + * + * - Once we reach an empty children, instead of continuing to insert + * new nodes for each remaining character of the OID, we store a pointer + * to the tail in the leaf; if the leaf is reached again, we turn it + * into a normal node and use the tail to create a new leaf. + * + * This is a pretty good balance between performance and memory usage. + */ +int git_oid_shorten_add(git_oid_shorten *os, const char *text_oid) +{ + int i; + bool is_leaf; + node_index idx; + + if (os->full) { + git_error_set(GIT_ERROR_INVALID, "unable to shorten OID - OID set full"); + return -1; + } + + if (text_oid == NULL) + return os->min_length; + + idx = 0; + is_leaf = false; + + for (i = 0; i < GIT_OID_SHA1_HEXSIZE; ++i) { + int c = git__fromhex(text_oid[i]); + trie_node *node; + + if (c == -1) { + git_error_set(GIT_ERROR_INVALID, "unable to shorten OID - invalid hex value"); + return -1; + } + + node = &os->nodes[idx]; + + if (is_leaf) { + const char *tail; + + tail = node->tail; + node->tail = NULL; + + node = push_leaf(os, idx, git__fromhex(tail[0]), &tail[1]); + if (node == NULL) { + if (os->full) + git_error_set(GIT_ERROR_INVALID, "unable to shorten OID - OID set full"); + return -1; + } + } + + if (node->children[c] == 0) { + if (push_leaf(os, idx, c, &text_oid[i + 1]) == NULL) { + if (os->full) + git_error_set(GIT_ERROR_INVALID, "unable to shorten OID - OID set full"); + return -1; + } + break; + } + + idx = node->children[c]; + is_leaf = false; + + if (idx < 0) { + node->children[c] = idx = -idx; + is_leaf = true; + } + } + + if (++i > os->min_length) + os->min_length = i; + + return os->min_length; +} + diff --git a/src/libgit2/oid.h b/src/libgit2/oid.h new file mode 100644 index 0000000..7b6b09d --- /dev/null +++ b/src/libgit2/oid.h @@ -0,0 +1,273 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_oid_h__ +#define INCLUDE_oid_h__ + +#include "common.h" + +#include "git2/experimental.h" +#include "git2/oid.h" +#include "hash.h" + +#ifdef GIT_EXPERIMENTAL_SHA256 +# define GIT_OID_NONE { 0, { 0 } } +# define GIT_OID_INIT(type, ...) { type, __VA_ARGS__ } +#else +# define GIT_OID_NONE { { 0 } } +# define GIT_OID_INIT(type, ...) { __VA_ARGS__ } +#endif + +extern const git_oid git_oid__empty_blob_sha1; +extern const git_oid git_oid__empty_tree_sha1; + +GIT_INLINE(git_oid_t) git_oid_type(const git_oid *oid) +{ +#ifdef GIT_EXPERIMENTAL_SHA256 + return oid->type; +#else + GIT_UNUSED(oid); + return GIT_OID_SHA1; +#endif +} + +GIT_INLINE(size_t) git_oid_size(git_oid_t type) +{ + switch (type) { + case GIT_OID_SHA1: + return GIT_OID_SHA1_SIZE; + +#ifdef GIT_EXPERIMENTAL_SHA256 + case GIT_OID_SHA256: + return GIT_OID_SHA256_SIZE; +#endif + + } + + return 0; +} + +GIT_INLINE(size_t) git_oid_hexsize(git_oid_t type) +{ + switch (type) { + case GIT_OID_SHA1: + return GIT_OID_SHA1_HEXSIZE; + +#ifdef GIT_EXPERIMENTAL_SHA256 + case GIT_OID_SHA256: + return GIT_OID_SHA256_HEXSIZE; +#endif + + } + + return 0; +} + +GIT_INLINE(const char *) git_oid_type_name(git_oid_t type) +{ + switch (type) { + case GIT_OID_SHA1: + return "sha1"; + +#ifdef GIT_EXPERIMENTAL_SHA256 + case GIT_OID_SHA256: + return "sha256"; +#endif + } + + return "unknown"; +} + +GIT_INLINE(git_oid_t) git_oid_type_fromstr(const char *name) +{ + if (strcmp(name, "sha1") == 0) + return GIT_OID_SHA1; + +#ifdef GIT_EXPERIMENTAL_SHA256 + if (strcmp(name, "sha256") == 0) + return GIT_OID_SHA256; +#endif + + return 0; +} + +GIT_INLINE(git_oid_t) git_oid_type_fromstrn(const char *name, size_t len) +{ + if (len == CONST_STRLEN("sha1") && strncmp(name, "sha1", len) == 0) + return GIT_OID_SHA1; + +#ifdef GIT_EXPERIMENTAL_SHA256 + if (len == CONST_STRLEN("sha256") && strncmp(name, "sha256", len) == 0) + return GIT_OID_SHA256; +#endif + + return 0; +} + +GIT_INLINE(git_hash_algorithm_t) git_oid_algorithm(git_oid_t type) +{ + switch (type) { + case GIT_OID_SHA1: + return GIT_HASH_ALGORITHM_SHA1; + +#ifdef GIT_EXPERIMENTAL_SHA256 + case GIT_OID_SHA256: + return GIT_HASH_ALGORITHM_SHA256; +#endif + + } + + return 0; +} + +/** + * Format a git_oid into a newly allocated c-string. + * + * The c-string is owned by the caller and needs to be manually freed. + * + * @param id the oid structure to format + * @return the c-string; NULL if memory is exhausted. Caller must + * deallocate the string with git__free(). + */ +char *git_oid_allocfmt(const git_oid *id); + +/** + * Format the requested nibbles of an object id. + * + * @param str the string to write into + * @param oid the oid structure to format + * @param start the starting number of nibbles + * @param count the number of nibbles to format + */ +GIT_INLINE(void) git_oid_fmt_substr( + char *str, + const git_oid *oid, + size_t start, + size_t count) +{ + static char hex[] = "0123456789abcdef"; + size_t i, end = start + count, min = start / 2, max = end / 2; + + if (start & 1) + *str++ = hex[oid->id[min++] & 0x0f]; + + for (i = min; i < max; i++) { + *str++ = hex[oid->id[i] >> 4]; + *str++ = hex[oid->id[i] & 0x0f]; + } + + if (end & 1) + *str++ = hex[oid->id[i] >> 4]; +} + +GIT_INLINE(int) git_oid_raw_ncmp( + const unsigned char *sha1, + const unsigned char *sha2, + size_t len) +{ + if (len > GIT_OID_MAX_HEXSIZE) + len = GIT_OID_MAX_HEXSIZE; + + while (len > 1) { + if (*sha1 != *sha2) + return 1; + sha1++; + sha2++; + len -= 2; + }; + + if (len) + if ((*sha1 ^ *sha2) & 0xf0) + return 1; + + return 0; +} + +GIT_INLINE(int) git_oid_raw_cmp( + const unsigned char *sha1, + const unsigned char *sha2, + size_t size) +{ + return memcmp(sha1, sha2, size); +} + +GIT_INLINE(int) git_oid_raw_cpy( + unsigned char *dst, + const unsigned char *src, + size_t size) +{ + memcpy(dst, src, size); + return 0; +} + +/* + * Compare two oid structures. + * + * @param a first oid structure. + * @param b second oid structure. + * @return <0, 0, >0 if a < b, a == b, a > b. + */ +GIT_INLINE(int) git_oid__cmp(const git_oid *a, const git_oid *b) +{ +#ifdef GIT_EXPERIMENTAL_SHA256 + if (a->type != b->type) + return a->type - b->type; + + return git_oid_raw_cmp(a->id, b->id, git_oid_size(a->type)); +#else + return git_oid_raw_cmp(a->id, b->id, git_oid_size(GIT_OID_SHA1)); +#endif +} + +GIT_INLINE(void) git_oid__cpy_prefix( + git_oid *out, const git_oid *id, size_t len) +{ +#ifdef GIT_EXPERIMENTAL_SHA256 + out->type = id->type; +#endif + + memcpy(&out->id, id->id, (len + 1) / 2); + + if (len & 1) + out->id[len / 2] &= 0xF0; +} + +GIT_INLINE(bool) git_oid__is_hexstr(const char *str, git_oid_t type) +{ + size_t i; + + for (i = 0; str[i] != '\0'; i++) { + if (git__fromhex(str[i]) < 0) + return false; + } + + return (i == git_oid_hexsize(type)); +} + +GIT_INLINE(void) git_oid_clear(git_oid *out, git_oid_t type) +{ + memset(out->id, 0, git_oid_size(type)); + +#ifdef GIT_EXPERIMENTAL_SHA256 + out->type = type; +#endif +} + +/* SHA256 support */ + +int git_oid__fromstr(git_oid *out, const char *str, git_oid_t type); + +int git_oid__fromstrp(git_oid *out, const char *str, git_oid_t type); + +int git_oid__fromstrn( + git_oid *out, + const char *str, + size_t length, + git_oid_t type); + +int git_oid__fromraw(git_oid *out, const unsigned char *raw, git_oid_t type); + +#endif diff --git a/src/libgit2/oidarray.c b/src/libgit2/oidarray.c new file mode 100644 index 0000000..37f6775 --- /dev/null +++ b/src/libgit2/oidarray.c @@ -0,0 +1,89 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "oidarray.h" + +#include "git2/oidarray.h" +#include "array.h" + +void git_oidarray_dispose(git_oidarray *arr) +{ + git__free(arr->ids); +} + +void git_oidarray__from_array(git_oidarray *out, const git_array_oid_t *array) +{ + out->count = array->size; + out->ids = array->ptr; +} + +void git_oidarray__to_array(git_array_oid_t *out, const git_oidarray *array) +{ + out->ptr = array->ids; + out->size = array->count; + out->asize = array->count; +} + +void git_oidarray__reverse(git_oidarray *arr) +{ + size_t i; + git_oid tmp; + + for (i = 0; i < arr->count / 2; i++) { + git_oid_cpy(&tmp, &arr->ids[i]); + git_oid_cpy(&arr->ids[i], &arr->ids[(arr->count-1)-i]); + git_oid_cpy(&arr->ids[(arr->count-1)-i], &tmp); + } +} + +int git_oidarray__add(git_array_oid_t *arr, git_oid *id) +{ + git_oid *add, *iter; + size_t i; + + git_array_foreach(*arr, i, iter) { + if (git_oid_cmp(iter, id) == 0) + return 0; + } + + if ((add = git_array_alloc(*arr)) == NULL) + return -1; + + git_oid_cpy(add, id); + return 0; +} + +bool git_oidarray__remove(git_array_oid_t *arr, git_oid *id) +{ + bool found = false; + size_t remain, i; + git_oid *iter; + + git_array_foreach(*arr, i, iter) { + if (git_oid_cmp(iter, id) == 0) { + arr->size--; + remain = arr->size - i; + + if (remain > 0) + memmove(&arr->ptr[i], &arr->ptr[i+1], remain * sizeof(git_oid)); + + found = true; + break; + } + } + + return found; +} + +#ifndef GIT_DEPRECATE_HARD + +void git_oidarray_free(git_oidarray *arr) +{ + git_oidarray_dispose(arr); +} + +#endif diff --git a/src/libgit2/oidarray.h b/src/libgit2/oidarray.h new file mode 100644 index 0000000..8f1543a --- /dev/null +++ b/src/libgit2/oidarray.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_oidarray_h__ +#define INCLUDE_oidarray_h__ + +#include "common.h" + +#include "git2/oidarray.h" +#include "array.h" + +typedef git_array_t(git_oid) git_array_oid_t; + +extern void git_oidarray__reverse(git_oidarray *arr); +extern void git_oidarray__from_array(git_oidarray *out, const git_array_oid_t *array); +extern void git_oidarray__to_array(git_array_oid_t *out, const git_oidarray *array); + +int git_oidarray__add(git_array_oid_t *arr, git_oid *id); +bool git_oidarray__remove(git_array_oid_t *arr, git_oid *id); + +#endif diff --git a/src/libgit2/oidmap.c b/src/libgit2/oidmap.c new file mode 100644 index 0000000..eaf9fa0 --- /dev/null +++ b/src/libgit2/oidmap.c @@ -0,0 +1,107 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "oidmap.h" + +#define kmalloc git__malloc +#define kcalloc git__calloc +#define krealloc git__realloc +#define kreallocarray git__reallocarray +#define kfree git__free +#include "khash.h" + +__KHASH_TYPE(oid, const git_oid *, void *) + +GIT_INLINE(khint_t) git_oidmap_hash(const git_oid *oid) +{ + khint_t h; + memcpy(&h, oid->id, sizeof(khint_t)); + return h; +} + +__KHASH_IMPL(oid, static kh_inline, const git_oid *, void *, 1, git_oidmap_hash, git_oid_equal) + +int git_oidmap_new(git_oidmap **out) +{ + *out = kh_init(oid); + GIT_ERROR_CHECK_ALLOC(*out); + + return 0; +} + +void git_oidmap_free(git_oidmap *map) +{ + kh_destroy(oid, map); +} + +void git_oidmap_clear(git_oidmap *map) +{ + kh_clear(oid, map); +} + +size_t git_oidmap_size(git_oidmap *map) +{ + return kh_size(map); +} + +void *git_oidmap_get(git_oidmap *map, const git_oid *key) +{ + size_t idx = kh_get(oid, map, key); + if (idx == kh_end(map) || !kh_exist(map, idx)) + return NULL; + return kh_val(map, idx); +} + +int git_oidmap_set(git_oidmap *map, const git_oid *key, void *value) +{ + size_t idx; + int rval; + + idx = kh_put(oid, map, key, &rval); + if (rval < 0) + return -1; + + if (rval == 0) + kh_key(map, idx) = key; + + kh_val(map, idx) = value; + + return 0; +} + +int git_oidmap_delete(git_oidmap *map, const git_oid *key) +{ + khiter_t idx = kh_get(oid, map, key); + if (idx == kh_end(map)) + return GIT_ENOTFOUND; + kh_del(oid, map, idx); + return 0; +} + +int git_oidmap_exists(git_oidmap *map, const git_oid *key) +{ + return kh_get(oid, map, key) != kh_end(map); +} + +int git_oidmap_iterate(void **value, git_oidmap *map, size_t *iter, const git_oid **key) +{ + size_t i = *iter; + + while (i < map->n_buckets && !kh_exist(map, i)) + i++; + + if (i >= map->n_buckets) + return GIT_ITEROVER; + + if (key) + *key = kh_key(map, i); + if (value) + *value = kh_value(map, i); + *iter = ++i; + + return 0; +} diff --git a/src/libgit2/oidmap.h b/src/libgit2/oidmap.h new file mode 100644 index 0000000..b748f72 --- /dev/null +++ b/src/libgit2/oidmap.h @@ -0,0 +1,128 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_oidmap_h__ +#define INCLUDE_oidmap_h__ + +#include "common.h" + +#include "git2/oid.h" + +/** A map with `git_oid`s as key. */ +typedef struct kh_oid_s git_oidmap; + +/** + * Allocate a new OID map. + * + * @param out Pointer to the map that shall be allocated. + * @return 0 on success, an error code if allocation has failed. + */ +int git_oidmap_new(git_oidmap **out); + +/** + * Free memory associated with the map. + * + * Note that this function will _not_ free values added to this + * map. + * + * @param map Pointer to the map that is to be free'd. May be + * `NULL`. + */ +void git_oidmap_free(git_oidmap *map); + +/** + * Clear all entries from the map. + * + * This function will remove all entries from the associated map. + * Memory associated with it will not be released, though. + * + * @param map Pointer to the map that shall be cleared. May be + * `NULL`. + */ +void git_oidmap_clear(git_oidmap *map); + +/** + * Return the number of elements in the map. + * + * @parameter map map containing the elements + * @return number of elements in the map + */ +size_t git_oidmap_size(git_oidmap *map); + +/** + * Return value associated with the given key. + * + * @param map map to search key in + * @param key key to search for + * @return value associated with the given key or NULL if the key was not found + */ +void *git_oidmap_get(git_oidmap *map, const git_oid *key); + +/** + * Set the entry for key to value. + * + * If the map has no corresponding entry for the given key, a new + * entry will be created with the given value. If an entry exists + * already, its value will be updated to match the given value. + * + * @param map map to create new entry in + * @param key key to set + * @param value value to associate the key with; may be NULL + * @return zero if the key was successfully set, a negative error + * code otherwise + */ +int git_oidmap_set(git_oidmap *map, const git_oid *key, void *value); + +/** + * Delete an entry from the map. + * + * Delete the given key and its value from the map. If no such + * key exists, this will do nothing. + * + * @param map map to delete key in + * @param key key to delete + * @return `0` if the key has been deleted, GIT_ENOTFOUND if no + * such key was found, a negative code in case of an + * error + */ +int git_oidmap_delete(git_oidmap *map, const git_oid *key); + +/** + * Check whether a key exists in the given map. + * + * @param map map to query for the key + * @param key key to search for + * @return 0 if the key has not been found, 1 otherwise + */ +int git_oidmap_exists(git_oidmap *map, const git_oid *key); + +/** + * Iterate over entries of the map. + * + * This functions allows to iterate over all key-value entries of + * the map. The current position is stored in the `iter` variable + * and should be initialized to `0` before the first call to this + * function. + * + * @param map map to iterate over + * @param value pointer to the variable where to store the current + * value. May be NULL. + * @param iter iterator storing the current position. Initialize + * with zero previous to the first call. + * @param key pointer to the variable where to store the current + * key. May be NULL. + * @return `0` if the next entry was correctly retrieved. + * GIT_ITEROVER if no entries are left. A negative error + * code otherwise. + */ +int git_oidmap_iterate(void **value, git_oidmap *map, size_t *iter, const git_oid **key); + +#define git_oidmap_foreach_value(h, vvar, code) { size_t __i = 0; \ + while (git_oidmap_iterate((void **) &(vvar), h, &__i, NULL) == 0) { \ + code; \ + } } + +#endif diff --git a/src/libgit2/pack-objects.c b/src/libgit2/pack-objects.c new file mode 100644 index 0000000..b2d80cb --- /dev/null +++ b/src/libgit2/pack-objects.c @@ -0,0 +1,1839 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "pack-objects.h" + +#include "buf.h" +#include "zstream.h" +#include "delta.h" +#include "iterator.h" +#include "pack.h" +#include "thread.h" +#include "tree.h" +#include "util.h" +#include "revwalk.h" +#include "commit_list.h" + +#include "git2/pack.h" +#include "git2/commit.h" +#include "git2/tag.h" +#include "git2/indexer.h" +#include "git2/config.h" + +struct unpacked { + git_pobject *object; + void *data; + struct git_delta_index *index; + size_t depth; +}; + +struct tree_walk_context { + git_packbuilder *pb; + git_str buf; +}; + +struct pack_write_context { + git_indexer *indexer; + git_indexer_progress *stats; +}; + +struct walk_object { + git_oid id; + unsigned int uninteresting:1, + seen:1; +}; + +#ifdef GIT_THREADS +# define GIT_PACKBUILDER__MUTEX_OP(pb, mtx, op) git_mutex_##op(&(pb)->mtx) +#else +# define GIT_PACKBUILDER__MUTEX_OP(pb, mtx, op) git__noop() +#endif + +#define git_packbuilder__cache_lock(pb) GIT_PACKBUILDER__MUTEX_OP(pb, cache_mutex, lock) +#define git_packbuilder__cache_unlock(pb) GIT_PACKBUILDER__MUTEX_OP(pb, cache_mutex, unlock) +#define git_packbuilder__progress_lock(pb) GIT_PACKBUILDER__MUTEX_OP(pb, progress_mutex, lock) +#define git_packbuilder__progress_unlock(pb) GIT_PACKBUILDER__MUTEX_OP(pb, progress_mutex, unlock) + +/* The minimal interval between progress updates (in seconds). */ +#define MIN_PROGRESS_UPDATE_INTERVAL 0.5 + +/* Size of the buffer to feed to zlib */ +#define COMPRESS_BUFLEN (1024 * 1024) + +static unsigned name_hash(const char *name) +{ + unsigned c, hash = 0; + + if (!name) + return 0; + + /* + * This effectively just creates a sortable number from the + * last sixteen non-whitespace characters. Last characters + * count "most", so things that end in ".c" sort together. + */ + while ((c = *name++) != 0) { + if (git__isspace(c)) + continue; + hash = (hash >> 2) + (c << 24); + } + return hash; +} + +static int packbuilder_config(git_packbuilder *pb) +{ + git_config *config; + int ret = 0; + int64_t val; + + if ((ret = git_repository_config_snapshot(&config, pb->repo)) < 0) + return ret; + +#define config_get(KEY,DST,DFLT) do { \ + ret = git_config_get_int64(&val, config, KEY); \ + if (!ret) { \ + if (!git__is_sizet(val)) { \ + git_error_set(GIT_ERROR_CONFIG, \ + "configuration value '%s' is too large", KEY); \ + ret = -1; \ + goto out; \ + } \ + (DST) = (size_t)val; \ + } else if (ret == GIT_ENOTFOUND) { \ + (DST) = (DFLT); \ + ret = 0; \ + } else if (ret < 0) goto out; } while (0) + + config_get("pack.deltaCacheSize", pb->max_delta_cache_size, + GIT_PACK_DELTA_CACHE_SIZE); + config_get("pack.deltaCacheLimit", pb->cache_max_small_delta_size, + GIT_PACK_DELTA_CACHE_LIMIT); + config_get("pack.deltaCacheSize", pb->big_file_threshold, + GIT_PACK_BIG_FILE_THRESHOLD); + config_get("pack.windowMemory", pb->window_memory_limit, 0); + +#undef config_get + +out: + git_config_free(config); + + return ret; +} + +int git_packbuilder_new(git_packbuilder **out, git_repository *repo) +{ + git_hash_algorithm_t hash_algorithm; + git_packbuilder *pb; + + *out = NULL; + + pb = git__calloc(1, sizeof(*pb)); + GIT_ERROR_CHECK_ALLOC(pb); + + pb->oid_type = repo->oid_type; + + hash_algorithm = git_oid_algorithm(pb->oid_type); + GIT_ASSERT(hash_algorithm); + + if (git_oidmap_new(&pb->object_ix) < 0 || + git_oidmap_new(&pb->walk_objects) < 0 || + git_pool_init(&pb->object_pool, sizeof(struct walk_object)) < 0) + goto on_error; + + pb->repo = repo; + pb->nr_threads = 1; /* do not spawn any thread by default */ + + if (git_hash_ctx_init(&pb->ctx, hash_algorithm) < 0 || + git_zstream_init(&pb->zstream, GIT_ZSTREAM_DEFLATE) < 0 || + git_repository_odb(&pb->odb, repo) < 0 || + packbuilder_config(pb) < 0) + goto on_error; + +#ifdef GIT_THREADS + + if (git_mutex_init(&pb->cache_mutex) || + git_mutex_init(&pb->progress_mutex) || + git_cond_init(&pb->progress_cond)) + { + git_error_set(GIT_ERROR_OS, "failed to initialize packbuilder mutex"); + goto on_error; + } + +#endif + + *out = pb; + return 0; + +on_error: + git_packbuilder_free(pb); + return -1; +} + +unsigned int git_packbuilder_set_threads(git_packbuilder *pb, unsigned int n) +{ + GIT_ASSERT_ARG(pb); + +#ifdef GIT_THREADS + pb->nr_threads = n; +#else + GIT_UNUSED(n); + GIT_ASSERT(pb->nr_threads == 1); +#endif + + return pb->nr_threads; +} + +static int rehash(git_packbuilder *pb) +{ + git_pobject *po; + size_t i; + + git_oidmap_clear(pb->object_ix); + + for (i = 0, po = pb->object_list; i < pb->nr_objects; i++, po++) { + if (git_oidmap_set(pb->object_ix, &po->id, po) < 0) + return -1; + } + + return 0; +} + +int git_packbuilder_insert(git_packbuilder *pb, const git_oid *oid, + const char *name) +{ + git_pobject *po; + size_t newsize; + int ret; + + GIT_ASSERT_ARG(pb); + GIT_ASSERT_ARG(oid); + + /* If the object already exists in the hash table, then we don't + * have any work to do */ + if (git_oidmap_exists(pb->object_ix, oid)) + return 0; + + if (pb->nr_objects >= pb->nr_alloc) { + GIT_ERROR_CHECK_ALLOC_ADD(&newsize, pb->nr_alloc, 1024); + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&newsize, newsize / 2, 3); + + if (!git__is_uint32(newsize)) { + git_error_set(GIT_ERROR_NOMEMORY, "packfile too large to fit in memory."); + return -1; + } + + pb->nr_alloc = newsize; + + pb->object_list = git__reallocarray(pb->object_list, + pb->nr_alloc, sizeof(*po)); + GIT_ERROR_CHECK_ALLOC(pb->object_list); + + if (rehash(pb) < 0) + return -1; + } + + po = pb->object_list + pb->nr_objects; + memset(po, 0x0, sizeof(*po)); + + if ((ret = git_odb_read_header(&po->size, &po->type, pb->odb, oid)) < 0) + return ret; + + pb->nr_objects++; + git_oid_cpy(&po->id, oid); + po->hash = name_hash(name); + + if (git_oidmap_set(pb->object_ix, &po->id, po) < 0) { + git_error_set_oom(); + return -1; + } + + pb->done = false; + + if (pb->progress_cb) { + uint64_t current_time = git_time_monotonic(); + uint64_t elapsed = current_time - pb->last_progress_report_time; + + if (elapsed >= MIN_PROGRESS_UPDATE_INTERVAL) { + pb->last_progress_report_time = current_time; + + ret = pb->progress_cb( + GIT_PACKBUILDER_ADDING_OBJECTS, + pb->nr_objects, 0, pb->progress_cb_payload); + + if (ret) + return git_error_set_after_callback(ret); + } + } + + return 0; +} + +static int get_delta(void **out, git_odb *odb, git_pobject *po) +{ + git_odb_object *src = NULL, *trg = NULL; + size_t delta_size; + void *delta_buf; + int error; + + *out = NULL; + + if (git_odb_read(&src, odb, &po->delta->id) < 0 || + git_odb_read(&trg, odb, &po->id) < 0) + goto on_error; + + error = git_delta(&delta_buf, &delta_size, + git_odb_object_data(src), git_odb_object_size(src), + git_odb_object_data(trg), git_odb_object_size(trg), + 0); + + if (error < 0 && error != GIT_EBUFS) + goto on_error; + + if (error == GIT_EBUFS || delta_size != po->delta_size) { + git_error_set(GIT_ERROR_INVALID, "delta size changed"); + goto on_error; + } + + *out = delta_buf; + + git_odb_object_free(src); + git_odb_object_free(trg); + return 0; + +on_error: + git_odb_object_free(src); + git_odb_object_free(trg); + return -1; +} + +static int write_object( + git_packbuilder *pb, + git_pobject *po, + int (*write_cb)(void *buf, size_t size, void *cb_data), + void *cb_data) +{ + git_odb_object *obj = NULL; + git_object_t type; + unsigned char hdr[10], *zbuf = NULL; + void *data = NULL; + size_t hdr_len, zbuf_len = COMPRESS_BUFLEN, data_len, oid_size; + int error; + + oid_size = git_oid_size(pb->oid_type); + + /* + * If we have a delta base, let's use the delta to save space. + * Otherwise load the whole object. 'data' ends up pointing to + * whatever data we want to put into the packfile. + */ + if (po->delta) { + if (po->delta_data) + data = po->delta_data; + else if ((error = get_delta(&data, pb->odb, po)) < 0) + goto done; + + data_len = po->delta_size; + type = GIT_OBJECT_REF_DELTA; + } else { + if ((error = git_odb_read(&obj, pb->odb, &po->id)) < 0) + goto done; + + data = (void *)git_odb_object_data(obj); + data_len = git_odb_object_size(obj); + type = git_odb_object_type(obj); + } + + /* Write header */ + if ((error = git_packfile__object_header(&hdr_len, hdr, data_len, type)) < 0 || + (error = write_cb(hdr, hdr_len, cb_data)) < 0 || + (error = git_hash_update(&pb->ctx, hdr, hdr_len)) < 0) + goto done; + + if (type == GIT_OBJECT_REF_DELTA) { + if ((error = write_cb(po->delta->id.id, oid_size, cb_data)) < 0 || + (error = git_hash_update(&pb->ctx, po->delta->id.id, oid_size)) < 0) + goto done; + } + + /* Write data */ + if (po->z_delta_size) { + data_len = po->z_delta_size; + + if ((error = write_cb(data, data_len, cb_data)) < 0 || + (error = git_hash_update(&pb->ctx, data, data_len)) < 0) + goto done; + } else { + zbuf = git__malloc(zbuf_len); + GIT_ERROR_CHECK_ALLOC(zbuf); + + git_zstream_reset(&pb->zstream); + + if ((error = git_zstream_set_input(&pb->zstream, data, data_len)) < 0) + goto done; + + while (!git_zstream_done(&pb->zstream)) { + if ((error = git_zstream_get_output(zbuf, &zbuf_len, &pb->zstream)) < 0 || + (error = write_cb(zbuf, zbuf_len, cb_data)) < 0 || + (error = git_hash_update(&pb->ctx, zbuf, zbuf_len)) < 0) + goto done; + + zbuf_len = COMPRESS_BUFLEN; /* reuse buffer */ + } + } + + /* + * If po->delta is true, data is a delta and it is our + * responsibility to free it (otherwise it's a git_object's + * data). We set po->delta_data to NULL in case we got the + * data from there instead of get_delta(). If we didn't, + * there's no harm. + */ + if (po->delta) { + git__free(data); + po->delta_data = NULL; + } + + pb->nr_written++; + +done: + git__free(zbuf); + git_odb_object_free(obj); + return error; +} + +enum write_one_status { + WRITE_ONE_SKIP = -1, /* already written */ + WRITE_ONE_BREAK = 0, /* writing this will bust the limit; not written */ + WRITE_ONE_WRITTEN = 1, /* normal */ + WRITE_ONE_RECURSIVE = 2 /* already scheduled to be written */ +}; + +static int write_one( + enum write_one_status *status, + git_packbuilder *pb, + git_pobject *po, + int (*write_cb)(void *buf, size_t size, void *cb_data), + void *cb_data) +{ + int error; + + if (po->recursing) { + *status = WRITE_ONE_RECURSIVE; + return 0; + } else if (po->written) { + *status = WRITE_ONE_SKIP; + return 0; + } + + if (po->delta) { + po->recursing = 1; + + if ((error = write_one(status, pb, po->delta, write_cb, cb_data)) < 0) + return error; + + /* we cannot depend on this one */ + if (*status == WRITE_ONE_RECURSIVE) + po->delta = NULL; + } + + *status = WRITE_ONE_WRITTEN; + po->written = 1; + po->recursing = 0; + + return write_object(pb, po, write_cb, cb_data); +} + +GIT_INLINE(void) add_to_write_order(git_pobject **wo, size_t *endp, + git_pobject *po) +{ + if (po->filled) + return; + wo[(*endp)++] = po; + po->filled = 1; +} + +static void add_descendants_to_write_order(git_pobject **wo, size_t *endp, + git_pobject *po) +{ + int add_to_order = 1; + while (po) { + if (add_to_order) { + git_pobject *s; + /* add this node... */ + add_to_write_order(wo, endp, po); + /* all its siblings... */ + for (s = po->delta_sibling; s; s = s->delta_sibling) { + add_to_write_order(wo, endp, s); + } + } + /* drop down a level to add left subtree nodes if possible */ + if (po->delta_child) { + add_to_order = 1; + po = po->delta_child; + } else { + add_to_order = 0; + /* our sibling might have some children, it is next */ + if (po->delta_sibling) { + po = po->delta_sibling; + continue; + } + /* go back to our parent node */ + po = po->delta; + while (po && !po->delta_sibling) { + /* we're on the right side of a subtree, keep + * going up until we can go right again */ + po = po->delta; + } + if (!po) { + /* done- we hit our original root node */ + return; + } + /* pass it off to sibling at this level */ + po = po->delta_sibling; + } + }; +} + +static void add_family_to_write_order(git_pobject **wo, size_t *endp, + git_pobject *po) +{ + git_pobject *root; + + for (root = po; root->delta; root = root->delta) + ; /* nothing */ + add_descendants_to_write_order(wo, endp, root); +} + +static int cb_tag_foreach(const char *name, git_oid *oid, void *data) +{ + git_packbuilder *pb = data; + git_pobject *po; + + GIT_UNUSED(name); + + if ((po = git_oidmap_get(pb->object_ix, oid)) == NULL) + return 0; + + po->tagged = 1; + + /* TODO: peel objects */ + + return 0; +} + +static int compute_write_order(git_pobject ***out, git_packbuilder *pb) +{ + size_t i, wo_end, last_untagged; + git_pobject **wo; + + *out = NULL; + + if (!pb->nr_objects) + return 0; + + if ((wo = git__mallocarray(pb->nr_objects, sizeof(*wo))) == NULL) + return -1; + + for (i = 0; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + po->tagged = 0; + po->filled = 0; + po->delta_child = NULL; + po->delta_sibling = NULL; + } + + /* + * Fully connect delta_child/delta_sibling network. + * Make sure delta_sibling is sorted in the original + * recency order. + */ + for (i = pb->nr_objects; i > 0;) { + git_pobject *po = &pb->object_list[--i]; + if (!po->delta) + continue; + /* Mark me as the first child */ + po->delta_sibling = po->delta->delta_child; + po->delta->delta_child = po; + } + + /* + * Mark objects that are at the tip of tags. + */ + if (git_tag_foreach(pb->repo, &cb_tag_foreach, pb) < 0) { + git__free(wo); + return -1; + } + + /* + * Give the objects in the original recency order until + * we see a tagged tip. + */ + for (i = wo_end = 0; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + if (po->tagged) + break; + add_to_write_order(wo, &wo_end, po); + } + last_untagged = i; + + /* + * Then fill all the tagged tips. + */ + for (; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + if (po->tagged) + add_to_write_order(wo, &wo_end, po); + } + + /* + * And then all remaining commits and tags. + */ + for (i = last_untagged; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + if (po->type != GIT_OBJECT_COMMIT && + po->type != GIT_OBJECT_TAG) + continue; + add_to_write_order(wo, &wo_end, po); + } + + /* + * And then all the trees. + */ + for (i = last_untagged; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + if (po->type != GIT_OBJECT_TREE) + continue; + add_to_write_order(wo, &wo_end, po); + } + + /* + * Finally all the rest in really tight order + */ + for (i = last_untagged; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + if (!po->filled) + add_family_to_write_order(wo, &wo_end, po); + } + + if (wo_end != pb->nr_objects) { + git__free(wo); + git_error_set(GIT_ERROR_INVALID, "invalid write order"); + return -1; + } + + *out = wo; + return 0; +} + +static int write_pack(git_packbuilder *pb, + int (*write_cb)(void *buf, size_t size, void *cb_data), + void *cb_data) +{ + git_pobject **write_order; + git_pobject *po; + enum write_one_status status; + struct git_pack_header ph; + git_oid entry_oid; + size_t i = 0; + int error; + + if ((error = compute_write_order(&write_order, pb)) < 0) + return error; + + if (!git__is_uint32(pb->nr_objects)) { + git_error_set(GIT_ERROR_INVALID, "too many objects"); + error = -1; + goto done; + } + + /* Write pack header */ + ph.hdr_signature = htonl(PACK_SIGNATURE); + ph.hdr_version = htonl(PACK_VERSION); + ph.hdr_entries = htonl(pb->nr_objects); + + if ((error = write_cb(&ph, sizeof(ph), cb_data)) < 0 || + (error = git_hash_update(&pb->ctx, &ph, sizeof(ph))) < 0) + goto done; + + pb->nr_remaining = pb->nr_objects; + do { + pb->nr_written = 0; + for ( ; i < pb->nr_objects; ++i) { + po = write_order[i]; + + if ((error = write_one(&status, pb, po, write_cb, cb_data)) < 0) + goto done; + } + + pb->nr_remaining -= pb->nr_written; + } while (pb->nr_remaining && i < pb->nr_objects); + + if ((error = git_hash_final(entry_oid.id, &pb->ctx)) < 0) + goto done; + + error = write_cb(entry_oid.id, git_oid_size(pb->oid_type), cb_data); + +done: + /* if callback cancelled writing, we must still free delta_data */ + for ( ; i < pb->nr_objects; ++i) { + po = write_order[i]; + if (po->delta_data) { + git__free(po->delta_data); + po->delta_data = NULL; + } + } + + git__free(write_order); + return error; +} + +static int write_pack_buf(void *buf, size_t size, void *data) +{ + git_str *b = (git_str *)data; + return git_str_put(b, buf, size); +} + +static int type_size_sort(const void *_a, const void *_b) +{ + const git_pobject *a = (git_pobject *)_a; + const git_pobject *b = (git_pobject *)_b; + + if (a->type > b->type) + return -1; + if (a->type < b->type) + return 1; + if (a->hash > b->hash) + return -1; + if (a->hash < b->hash) + return 1; + /* + * TODO + * + if (a->preferred_base > b->preferred_base) + return -1; + if (a->preferred_base < b->preferred_base) + return 1; + */ + if (a->size > b->size) + return -1; + if (a->size < b->size) + return 1; + return a < b ? -1 : (a > b); /* newest first */ +} + +static int delta_cacheable( + git_packbuilder *pb, + size_t src_size, + size_t trg_size, + size_t delta_size) +{ + size_t new_size; + + if (git__add_sizet_overflow(&new_size, pb->delta_cache_size, delta_size)) + return 0; + + if (pb->max_delta_cache_size && new_size > pb->max_delta_cache_size) + return 0; + + if (delta_size < pb->cache_max_small_delta_size) + return 1; + + /* cache delta, if objects are large enough compared to delta size */ + if ((src_size >> 20) + (trg_size >> 21) > (delta_size >> 10)) + return 1; + + return 0; +} + +static int try_delta(git_packbuilder *pb, struct unpacked *trg, + struct unpacked *src, size_t max_depth, + size_t *mem_usage, int *ret) +{ + git_pobject *trg_object = trg->object; + git_pobject *src_object = src->object; + git_odb_object *obj; + size_t trg_size, src_size, delta_size, sizediff, max_size, sz; + size_t ref_depth; + void *delta_buf; + + /* Don't bother doing diffs between different types */ + if (trg_object->type != src_object->type) { + *ret = -1; + return 0; + } + + *ret = 0; + + /* TODO: support reuse-delta */ + + /* Let's not bust the allowed depth. */ + if (src->depth >= max_depth) + return 0; + + /* Now some size filtering heuristics. */ + trg_size = trg_object->size; + if (!trg_object->delta) { + max_size = trg_size/2 - 20; + ref_depth = 1; + } else { + max_size = trg_object->delta_size; + ref_depth = trg->depth; + } + + max_size = (uint64_t)max_size * (max_depth - src->depth) / + (max_depth - ref_depth + 1); + if (max_size == 0) + return 0; + + src_size = src_object->size; + sizediff = src_size < trg_size ? trg_size - src_size : 0; + if (sizediff >= max_size) + return 0; + if (trg_size < src_size / 32) + return 0; + + /* Load data if not already done */ + if (!trg->data) { + if (git_odb_read(&obj, pb->odb, &trg_object->id) < 0) + return -1; + + sz = git_odb_object_size(obj); + trg->data = git__malloc(sz); + GIT_ERROR_CHECK_ALLOC(trg->data); + memcpy(trg->data, git_odb_object_data(obj), sz); + + git_odb_object_free(obj); + + if (sz != trg_size) { + git_error_set(GIT_ERROR_INVALID, + "inconsistent target object length"); + return -1; + } + + *mem_usage += sz; + } + if (!src->data) { + size_t obj_sz; + + if (git_odb_read(&obj, pb->odb, &src_object->id) < 0 || + !git__is_ulong(obj_sz = git_odb_object_size(obj))) + return -1; + + sz = obj_sz; + src->data = git__malloc(sz); + GIT_ERROR_CHECK_ALLOC(src->data); + memcpy(src->data, git_odb_object_data(obj), sz); + + git_odb_object_free(obj); + + if (sz != src_size) { + git_error_set(GIT_ERROR_INVALID, + "inconsistent source object length"); + return -1; + } + + *mem_usage += sz; + } + if (!src->index) { + if (git_delta_index_init(&src->index, src->data, src_size) < 0) + return 0; /* suboptimal pack - out of memory */ + + *mem_usage += git_delta_index_size(src->index); + } + + if (git_delta_create_from_index(&delta_buf, &delta_size, src->index, trg->data, trg_size, + max_size) < 0) + return 0; + + if (trg_object->delta) { + /* Prefer only shallower same-sized deltas. */ + if (delta_size == trg_object->delta_size && + src->depth + 1 >= trg->depth) { + git__free(delta_buf); + return 0; + } + } + + GIT_ASSERT(git_packbuilder__cache_lock(pb) == 0); + + if (trg_object->delta_data) { + git__free(trg_object->delta_data); + GIT_ASSERT(pb->delta_cache_size >= trg_object->delta_size); + pb->delta_cache_size -= trg_object->delta_size; + trg_object->delta_data = NULL; + } + if (delta_cacheable(pb, src_size, trg_size, delta_size)) { + bool overflow = git__add_sizet_overflow( + &pb->delta_cache_size, pb->delta_cache_size, delta_size); + + GIT_ASSERT(git_packbuilder__cache_unlock(pb) == 0); + + if (overflow) { + git__free(delta_buf); + return -1; + } + + trg_object->delta_data = git__realloc(delta_buf, delta_size); + GIT_ERROR_CHECK_ALLOC(trg_object->delta_data); + } else { + /* create delta when writing the pack */ + GIT_ASSERT(git_packbuilder__cache_unlock(pb) == 0); + git__free(delta_buf); + } + + trg_object->delta = src_object; + trg_object->delta_size = delta_size; + trg->depth = src->depth + 1; + + *ret = 1; + return 0; +} + +static size_t check_delta_limit(git_pobject *me, size_t n) +{ + git_pobject *child = me->delta_child; + size_t m = n; + + while (child) { + size_t c = check_delta_limit(child, n + 1); + if (m < c) + m = c; + child = child->delta_sibling; + } + return m; +} + +static size_t free_unpacked(struct unpacked *n) +{ + size_t freed_mem = 0; + + if (n->index) { + freed_mem += git_delta_index_size(n->index); + git_delta_index_free(n->index); + } + n->index = NULL; + + if (n->data) { + freed_mem += n->object->size; + git__free(n->data); + n->data = NULL; + } + n->object = NULL; + n->depth = 0; + return freed_mem; +} + +static int report_delta_progress( + git_packbuilder *pb, uint32_t count, bool force) +{ + int ret; + + if (pb->progress_cb) { + uint64_t current_time = git_time_monotonic(); + uint64_t elapsed = current_time - pb->last_progress_report_time; + + if (force || elapsed >= MIN_PROGRESS_UPDATE_INTERVAL) { + pb->last_progress_report_time = current_time; + + ret = pb->progress_cb( + GIT_PACKBUILDER_DELTAFICATION, + count, pb->nr_objects, pb->progress_cb_payload); + + if (ret) + return git_error_set_after_callback(ret); + } + } + + return 0; +} + +static int find_deltas(git_packbuilder *pb, git_pobject **list, + size_t *list_size, size_t window, size_t depth) +{ + git_pobject *po; + git_str zbuf = GIT_STR_INIT; + struct unpacked *array; + size_t idx = 0, count = 0; + size_t mem_usage = 0; + size_t i; + int error = -1; + + array = git__calloc(window, sizeof(struct unpacked)); + GIT_ERROR_CHECK_ALLOC(array); + + for (;;) { + struct unpacked *n = array + idx; + size_t max_depth, j, best_base = SIZE_MAX; + + GIT_ASSERT(git_packbuilder__progress_lock(pb) == 0); + if (!*list_size) { + GIT_ASSERT(git_packbuilder__progress_unlock(pb) == 0); + break; + } + + pb->nr_deltified += 1; + report_delta_progress(pb, pb->nr_deltified, false); + + po = *list++; + (*list_size)--; + GIT_ASSERT(git_packbuilder__progress_unlock(pb) == 0); + + mem_usage -= free_unpacked(n); + n->object = po; + + while (pb->window_memory_limit && + mem_usage > pb->window_memory_limit && + count > 1) { + size_t tail = (idx + window - count) % window; + mem_usage -= free_unpacked(array + tail); + count--; + } + + /* + * If the current object is at pack edge, take the depth the + * objects that depend on the current object into account + * otherwise they would become too deep. + */ + max_depth = depth; + if (po->delta_child) { + size_t delta_limit = check_delta_limit(po, 0); + + if (delta_limit > max_depth) + goto next; + + max_depth -= delta_limit; + } + + j = window; + while (--j > 0) { + int ret; + size_t other_idx = idx + j; + struct unpacked *m; + + if (other_idx >= window) + other_idx -= window; + + m = array + other_idx; + if (!m->object) + break; + + if (try_delta(pb, n, m, max_depth, &mem_usage, &ret) < 0) + goto on_error; + if (ret < 0) + break; + else if (ret > 0) + best_base = other_idx; + } + + /* + * If we decided to cache the delta data, then it is best + * to compress it right away. First because we have to do + * it anyway, and doing it here while we're threaded will + * save a lot of time in the non threaded write phase, + * as well as allow for caching more deltas within + * the same cache size limit. + * ... + * But only if not writing to stdout, since in that case + * the network is most likely throttling writes anyway, + * and therefore it is best to go to the write phase ASAP + * instead, as we can afford spending more time compressing + * between writes at that moment. + */ + if (po->delta_data) { + if (git_zstream_deflatebuf(&zbuf, po->delta_data, po->delta_size) < 0) + goto on_error; + + git__free(po->delta_data); + po->delta_data = git__malloc(zbuf.size); + GIT_ERROR_CHECK_ALLOC(po->delta_data); + + memcpy(po->delta_data, zbuf.ptr, zbuf.size); + po->z_delta_size = zbuf.size; + git_str_clear(&zbuf); + + GIT_ASSERT(git_packbuilder__cache_lock(pb) == 0); + pb->delta_cache_size -= po->delta_size; + pb->delta_cache_size += po->z_delta_size; + GIT_ASSERT(git_packbuilder__cache_unlock(pb) == 0); + } + + /* + * If we made n a delta, and if n is already at max + * depth, leaving it in the window is pointless. we + * should evict it first. + */ + if (po->delta && max_depth <= n->depth) + continue; + + /* + * Move the best delta base up in the window, after the + * currently deltified object, to keep it longer. It will + * be the first base object to be attempted next. + */ + if (po->delta) { + struct unpacked swap = array[best_base]; + size_t dist = (window + idx - best_base) % window; + size_t dst = best_base; + while (dist--) { + size_t src = (dst + 1) % window; + array[dst] = array[src]; + dst = src; + } + array[dst] = swap; + } + + next: + idx++; + if (count + 1 < window) + count++; + if (idx >= window) + idx = 0; + } + error = 0; + +on_error: + for (i = 0; i < window; ++i) { + git__free(array[i].index); + git__free(array[i].data); + } + git__free(array); + git_str_dispose(&zbuf); + + return error; +} + +#ifdef GIT_THREADS + +struct thread_params { + git_thread thread; + git_packbuilder *pb; + + git_pobject **list; + + git_cond cond; + git_mutex mutex; + + size_t list_size; + size_t remaining; + + size_t window; + size_t depth; + size_t working; + size_t data_ready; +}; + +static void *threaded_find_deltas(void *arg) +{ + struct thread_params *me = arg; + + while (me->remaining) { + if (find_deltas(me->pb, me->list, &me->remaining, + me->window, me->depth) < 0) { + ; /* TODO */ + } + + GIT_ASSERT_WITH_RETVAL(git_packbuilder__progress_lock(me->pb) == 0, NULL); + me->working = 0; + git_cond_signal(&me->pb->progress_cond); + GIT_ASSERT_WITH_RETVAL(git_packbuilder__progress_unlock(me->pb) == 0, NULL); + + if (git_mutex_lock(&me->mutex)) { + git_error_set(GIT_ERROR_THREAD, "unable to lock packfile condition mutex"); + return NULL; + } + + while (!me->data_ready) + git_cond_wait(&me->cond, &me->mutex); + + /* + * We must not set ->data_ready before we wait on the + * condition because the main thread may have set it to 1 + * before we get here. In order to be sure that new + * work is available if we see 1 in ->data_ready, it + * was initialized to 0 before this thread was spawned + * and we reset it to 0 right away. + */ + me->data_ready = 0; + git_mutex_unlock(&me->mutex); + } + /* leave ->working 1 so that this doesn't get more work assigned */ + return NULL; +} + +static int ll_find_deltas(git_packbuilder *pb, git_pobject **list, + size_t list_size, size_t window, size_t depth) +{ + struct thread_params *p; + size_t i; + int ret, active_threads = 0; + + if (!pb->nr_threads) + pb->nr_threads = git__online_cpus(); + + if (pb->nr_threads <= 1) { + find_deltas(pb, list, &list_size, window, depth); + return 0; + } + + p = git__mallocarray(pb->nr_threads, sizeof(*p)); + GIT_ERROR_CHECK_ALLOC(p); + + /* Partition the work among the threads */ + for (i = 0; i < pb->nr_threads; ++i) { + size_t sub_size = list_size / (pb->nr_threads - i); + + /* don't use too small segments or no deltas will be found */ + if (sub_size < 2*window && i+1 < pb->nr_threads) + sub_size = 0; + + p[i].pb = pb; + p[i].window = window; + p[i].depth = depth; + p[i].working = 1; + p[i].data_ready = 0; + + /* try to split chunks on "path" boundaries */ + while (sub_size && sub_size < list_size && + list[sub_size]->hash && + list[sub_size]->hash == list[sub_size-1]->hash) + sub_size++; + + p[i].list = list; + p[i].list_size = sub_size; + p[i].remaining = sub_size; + + list += sub_size; + list_size -= sub_size; + } + + /* Start work threads */ + for (i = 0; i < pb->nr_threads; ++i) { + if (!p[i].list_size) + continue; + + git_mutex_init(&p[i].mutex); + git_cond_init(&p[i].cond); + + ret = git_thread_create(&p[i].thread, + threaded_find_deltas, &p[i]); + if (ret) { + git_error_set(GIT_ERROR_THREAD, "unable to create thread"); + return -1; + } + active_threads++; + } + + /* + * Now let's wait for work completion. Each time a thread is done + * with its work, we steal half of the remaining work from the + * thread with the largest number of unprocessed objects and give + * it to that newly idle thread. This ensure good load balancing + * until the remaining object list segments are simply too short + * to be worth splitting anymore. + */ + while (active_threads) { + struct thread_params *target = NULL; + struct thread_params *victim = NULL; + size_t sub_size = 0; + + /* Start by locating a thread that has transitioned its + * 'working' flag from 1 -> 0. This indicates that it is + * ready to receive more work using our work-stealing + * algorithm. */ + GIT_ASSERT(git_packbuilder__progress_lock(pb) == 0); + for (;;) { + for (i = 0; !target && i < pb->nr_threads; i++) + if (!p[i].working) + target = &p[i]; + if (target) + break; + git_cond_wait(&pb->progress_cond, &pb->progress_mutex); + } + + /* At this point we hold the progress lock and have located + * a thread to receive more work. We still need to locate a + * thread from which to steal work (the victim). */ + for (i = 0; i < pb->nr_threads; i++) + if (p[i].remaining > 2*window && + (!victim || victim->remaining < p[i].remaining)) + victim = &p[i]; + + if (victim) { + sub_size = victim->remaining / 2; + list = victim->list + victim->list_size - sub_size; + while (sub_size && list[0]->hash && + list[0]->hash == list[-1]->hash) { + list++; + sub_size--; + } + if (!sub_size) { + /* + * It is possible for some "paths" to have + * so many objects that no hash boundary + * might be found. Let's just steal the + * exact half in that case. + */ + sub_size = victim->remaining / 2; + list -= sub_size; + } + target->list = list; + victim->list_size -= sub_size; + victim->remaining -= sub_size; + } + target->list_size = sub_size; + target->remaining = sub_size; + target->working = 1; + GIT_ASSERT(git_packbuilder__progress_unlock(pb) == 0); + + if (git_mutex_lock(&target->mutex)) { + git_error_set(GIT_ERROR_THREAD, "unable to lock packfile condition mutex"); + git__free(p); + return -1; + } + + target->data_ready = 1; + git_cond_signal(&target->cond); + git_mutex_unlock(&target->mutex); + + if (!sub_size) { + git_thread_join(&target->thread, NULL); + git_cond_free(&target->cond); + git_mutex_free(&target->mutex); + active_threads--; + } + } + + git__free(p); + return 0; +} + +#else +#define ll_find_deltas(pb, l, ls, w, d) find_deltas(pb, l, &ls, w, d) +#endif + +int git_packbuilder__prepare(git_packbuilder *pb) +{ + git_pobject **delta_list; + size_t i, n = 0; + + if (pb->nr_objects == 0 || pb->done) + return 0; /* nothing to do */ + + /* + * Although we do not report progress during deltafication, we + * at least report that we are in the deltafication stage + */ + if (pb->progress_cb) + pb->progress_cb(GIT_PACKBUILDER_DELTAFICATION, 0, pb->nr_objects, pb->progress_cb_payload); + + delta_list = git__mallocarray(pb->nr_objects, sizeof(*delta_list)); + GIT_ERROR_CHECK_ALLOC(delta_list); + + for (i = 0; i < pb->nr_objects; ++i) { + git_pobject *po = pb->object_list + i; + + /* Make sure the item is within our size limits */ + if (po->size < 50 || po->size > pb->big_file_threshold) + continue; + + delta_list[n++] = po; + } + + if (n > 1) { + git__tsort((void **)delta_list, n, type_size_sort); + if (ll_find_deltas(pb, delta_list, n, + GIT_PACK_WINDOW + 1, + GIT_PACK_DEPTH) < 0) { + git__free(delta_list); + return -1; + } + } + + report_delta_progress(pb, pb->nr_objects, true); + + pb->done = true; + git__free(delta_list); + return 0; +} + +#define PREPARE_PACK if (git_packbuilder__prepare(pb) < 0) { return -1; } + +int git_packbuilder_foreach(git_packbuilder *pb, int (*cb)(void *buf, size_t size, void *payload), void *payload) +{ + PREPARE_PACK; + return write_pack(pb, cb, payload); +} + +int git_packbuilder__write_buf(git_str *buf, git_packbuilder *pb) +{ + PREPARE_PACK; + + return write_pack(pb, &write_pack_buf, buf); +} + +int git_packbuilder_write_buf(git_buf *buf, git_packbuilder *pb) +{ + GIT_BUF_WRAP_PRIVATE(buf, git_packbuilder__write_buf, pb); +} + +static int write_cb(void *buf, size_t len, void *payload) +{ + struct pack_write_context *ctx = payload; + return git_indexer_append(ctx->indexer, buf, len, ctx->stats); +} + +int git_packbuilder_write( + git_packbuilder *pb, + const char *path, + unsigned int mode, + git_indexer_progress_cb progress_cb, + void *progress_cb_payload) +{ + int error = -1; + git_str object_path = GIT_STR_INIT; + git_indexer_options opts = GIT_INDEXER_OPTIONS_INIT; + git_indexer *indexer = NULL; + git_indexer_progress stats; + struct pack_write_context ctx; + int t; + + PREPARE_PACK; + + if (path == NULL) { + if ((error = git_repository__item_path(&object_path, pb->repo, GIT_REPOSITORY_ITEM_OBJECTS)) < 0) + goto cleanup; + if ((error = git_str_joinpath(&object_path, git_str_cstr(&object_path), "pack")) < 0) + goto cleanup; + path = git_str_cstr(&object_path); + } + + opts.progress_cb = progress_cb; + opts.progress_cb_payload = progress_cb_payload; + + /* TODO: SHA256 */ + +#ifdef GIT_EXPERIMENTAL_SHA256 + opts.mode = mode; + opts.odb = pb->odb; + + error = git_indexer_new(&indexer, path, GIT_OID_SHA1, &opts); +#else + error = git_indexer_new(&indexer, path, mode, pb->odb, &opts); +#endif + + if (error < 0) + goto cleanup; + + if (!git_repository__configmap_lookup(&t, pb->repo, GIT_CONFIGMAP_FSYNCOBJECTFILES) && t) + git_indexer__set_fsync(indexer, 1); + + ctx.indexer = indexer; + ctx.stats = &stats; + + if ((error = git_packbuilder_foreach(pb, write_cb, &ctx)) < 0) + goto cleanup; + + if ((error = git_indexer_commit(indexer, &stats)) < 0) + goto cleanup; + +#ifndef GIT_DEPRECATE_HARD + git_oid_cpy(&pb->pack_oid, git_indexer_hash(indexer)); +#endif + + pb->pack_name = git__strdup(git_indexer_name(indexer)); + GIT_ERROR_CHECK_ALLOC(pb->pack_name); + +cleanup: + git_indexer_free(indexer); + git_str_dispose(&object_path); + return error; +} + +#undef PREPARE_PACK + +#ifndef GIT_DEPRECATE_HARD +const git_oid *git_packbuilder_hash(git_packbuilder *pb) +{ + return &pb->pack_oid; +} +#endif + +const char *git_packbuilder_name(git_packbuilder *pb) +{ + return pb->pack_name; +} + + +static int cb_tree_walk( + const char *root, const git_tree_entry *entry, void *payload) +{ + int error; + struct tree_walk_context *ctx = payload; + + /* A commit inside a tree represents a submodule commit and should be skipped. */ + if (git_tree_entry_type(entry) == GIT_OBJECT_COMMIT) + return 0; + + if (!(error = git_str_sets(&ctx->buf, root)) && + !(error = git_str_puts(&ctx->buf, git_tree_entry_name(entry)))) + error = git_packbuilder_insert( + ctx->pb, git_tree_entry_id(entry), git_str_cstr(&ctx->buf)); + + return error; +} + +int git_packbuilder_insert_commit(git_packbuilder *pb, const git_oid *oid) +{ + git_commit *commit; + + if (git_commit_lookup(&commit, pb->repo, oid) < 0 || + git_packbuilder_insert(pb, oid, NULL) < 0) + return -1; + + if (git_packbuilder_insert_tree(pb, git_commit_tree_id(commit)) < 0) + return -1; + + git_commit_free(commit); + return 0; +} + +int git_packbuilder_insert_tree(git_packbuilder *pb, const git_oid *oid) +{ + int error; + git_tree *tree = NULL; + struct tree_walk_context context = { pb, GIT_STR_INIT }; + + if (!(error = git_tree_lookup(&tree, pb->repo, oid)) && + !(error = git_packbuilder_insert(pb, oid, NULL))) + error = git_tree_walk(tree, GIT_TREEWALK_PRE, cb_tree_walk, &context); + + git_tree_free(tree); + git_str_dispose(&context.buf); + return error; +} + +int git_packbuilder_insert_recur(git_packbuilder *pb, const git_oid *id, const char *name) +{ + git_object *obj; + int error; + + GIT_ASSERT_ARG(pb); + GIT_ASSERT_ARG(id); + + if ((error = git_object_lookup(&obj, pb->repo, id, GIT_OBJECT_ANY)) < 0) + return error; + + switch (git_object_type(obj)) { + case GIT_OBJECT_BLOB: + error = git_packbuilder_insert(pb, id, name); + break; + case GIT_OBJECT_TREE: + error = git_packbuilder_insert_tree(pb, id); + break; + case GIT_OBJECT_COMMIT: + error = git_packbuilder_insert_commit(pb, id); + break; + case GIT_OBJECT_TAG: + if ((error = git_packbuilder_insert(pb, id, name)) < 0) + goto cleanup; + error = git_packbuilder_insert_recur(pb, git_tag_target_id((git_tag *) obj), NULL); + break; + + default: + git_error_set(GIT_ERROR_INVALID, "unknown object type"); + error = -1; + } + +cleanup: + git_object_free(obj); + return error; +} + +size_t git_packbuilder_object_count(git_packbuilder *pb) +{ + return pb->nr_objects; +} + +size_t git_packbuilder_written(git_packbuilder *pb) +{ + return pb->nr_written; +} + +static int lookup_walk_object(struct walk_object **out, git_packbuilder *pb, const git_oid *id) +{ + struct walk_object *obj; + + obj = git_pool_mallocz(&pb->object_pool, 1); + if (!obj) { + git_error_set_oom(); + return -1; + } + + git_oid_cpy(&obj->id, id); + + *out = obj; + return 0; +} + +static int retrieve_object(struct walk_object **out, git_packbuilder *pb, const git_oid *id) +{ + struct walk_object *obj; + int error; + + if ((obj = git_oidmap_get(pb->walk_objects, id)) == NULL) { + if ((error = lookup_walk_object(&obj, pb, id)) < 0) + return error; + + if ((error = git_oidmap_set(pb->walk_objects, &obj->id, obj)) < 0) + return error; + } + + *out = obj; + return 0; +} + +static int mark_blob_uninteresting(git_packbuilder *pb, const git_oid *id) +{ + int error; + struct walk_object *obj; + + if ((error = retrieve_object(&obj, pb, id)) < 0) + return error; + + obj->uninteresting = 1; + + return 0; +} + +static int mark_tree_uninteresting(git_packbuilder *pb, const git_oid *id) +{ + struct walk_object *obj; + git_tree *tree; + int error; + size_t i; + + if ((error = retrieve_object(&obj, pb, id)) < 0) + return error; + + if (obj->uninteresting) + return 0; + + obj->uninteresting = 1; + + if ((error = git_tree_lookup(&tree, pb->repo, id)) < 0) + return error; + + for (i = 0; i < git_tree_entrycount(tree); i++) { + const git_tree_entry *entry = git_tree_entry_byindex(tree, i); + const git_oid *entry_id = git_tree_entry_id(entry); + switch (git_tree_entry_type(entry)) { + case GIT_OBJECT_TREE: + if ((error = mark_tree_uninteresting(pb, entry_id)) < 0) + goto cleanup; + break; + case GIT_OBJECT_BLOB: + if ((error = mark_blob_uninteresting(pb, entry_id)) < 0) + goto cleanup; + break; + default: + /* it's a submodule or something unknown, we don't want it */ + ; + } + } + +cleanup: + git_tree_free(tree); + return error; +} + +/* + * Mark the edges of the graph uninteresting. Since we start from a + * git_revwalk, the commits are already uninteresting, but we need to + * mark the trees and blobs. + */ +static int mark_edges_uninteresting(git_packbuilder *pb, git_commit_list *commits) +{ + int error; + git_commit_list *list; + git_commit *commit; + + for (list = commits; list; list = list->next) { + if (!list->item->uninteresting) + continue; + + if ((error = git_commit_lookup(&commit, pb->repo, &list->item->oid)) < 0) + return error; + + error = mark_tree_uninteresting(pb, git_commit_tree_id(commit)); + git_commit_free(commit); + + if (error < 0) + return error; + } + + return 0; +} + +static int pack_objects_insert_tree(git_packbuilder *pb, git_tree *tree) +{ + size_t i; + int error; + git_tree *subtree; + struct walk_object *obj; + const char *name; + + if ((error = retrieve_object(&obj, pb, git_tree_id(tree))) < 0) + return error; + + if (obj->seen || obj->uninteresting) + return 0; + + obj->seen = 1; + + if ((error = git_packbuilder_insert(pb, &obj->id, NULL))) + return error; + + for (i = 0; i < git_tree_entrycount(tree); i++) { + const git_tree_entry *entry = git_tree_entry_byindex(tree, i); + const git_oid *entry_id = git_tree_entry_id(entry); + switch (git_tree_entry_type(entry)) { + case GIT_OBJECT_TREE: + if ((error = git_tree_lookup(&subtree, pb->repo, entry_id)) < 0) + return error; + + error = pack_objects_insert_tree(pb, subtree); + git_tree_free(subtree); + + if (error < 0) + return error; + + break; + case GIT_OBJECT_BLOB: + if ((error = retrieve_object(&obj, pb, entry_id)) < 0) + return error; + if (obj->uninteresting) + continue; + name = git_tree_entry_name(entry); + if ((error = git_packbuilder_insert(pb, entry_id, name)) < 0) + return error; + break; + default: + /* it's a submodule or something unknown, we don't want it */ + ; + } + } + + + return error; +} + +static int pack_objects_insert_commit(git_packbuilder *pb, struct walk_object *obj) +{ + int error; + git_commit *commit = NULL; + git_tree *tree = NULL; + + obj->seen = 1; + + if ((error = git_packbuilder_insert(pb, &obj->id, NULL)) < 0) + return error; + + if ((error = git_commit_lookup(&commit, pb->repo, &obj->id)) < 0) + return error; + + if ((error = git_tree_lookup(&tree, pb->repo, git_commit_tree_id(commit))) < 0) + goto cleanup; + + if ((error = pack_objects_insert_tree(pb, tree)) < 0) + goto cleanup; + +cleanup: + git_commit_free(commit); + git_tree_free(tree); + return error; +} + +int git_packbuilder_insert_walk(git_packbuilder *pb, git_revwalk *walk) +{ + int error; + git_oid id; + struct walk_object *obj; + + GIT_ASSERT_ARG(pb); + GIT_ASSERT_ARG(walk); + + if ((error = mark_edges_uninteresting(pb, walk->user_input)) < 0) + return error; + + /* + * TODO: git marks the parents of the edges + * uninteresting. This may provide a speed advantage, but does + * seem to assume the remote does not have a single-commit + * history on the other end. + */ + + /* walk down each tree up to the blobs and insert them, stopping when uninteresting */ + while ((error = git_revwalk_next(&id, walk)) == 0) { + if ((error = retrieve_object(&obj, pb, &id)) < 0) + return error; + + if (obj->seen || obj->uninteresting) + continue; + + if ((error = pack_objects_insert_commit(pb, obj)) < 0) + return error; + } + + if (error == GIT_ITEROVER) + error = 0; + + return error; +} + +int git_packbuilder_set_callbacks(git_packbuilder *pb, git_packbuilder_progress progress_cb, void *progress_cb_payload) +{ + if (!pb) + return -1; + + pb->progress_cb = progress_cb; + pb->progress_cb_payload = progress_cb_payload; + + return 0; +} + +void git_packbuilder_free(git_packbuilder *pb) +{ + if (pb == NULL) + return; + +#ifdef GIT_THREADS + + git_mutex_free(&pb->cache_mutex); + git_mutex_free(&pb->progress_mutex); + git_cond_free(&pb->progress_cond); + +#endif + + if (pb->odb) + git_odb_free(pb->odb); + + if (pb->object_ix) + git_oidmap_free(pb->object_ix); + + if (pb->object_list) + git__free(pb->object_list); + + git_oidmap_free(pb->walk_objects); + git_pool_clear(&pb->object_pool); + + git_hash_ctx_cleanup(&pb->ctx); + git_zstream_free(&pb->zstream); + + git__free(pb->pack_name); + + git__free(pb); +} diff --git a/src/libgit2/pack-objects.h b/src/libgit2/pack-objects.h new file mode 100644 index 0000000..bbc8b94 --- /dev/null +++ b/src/libgit2/pack-objects.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_pack_objects_h__ +#define INCLUDE_pack_objects_h__ + +#include "common.h" + +#include "str.h" +#include "hash.h" +#include "oidmap.h" +#include "zstream.h" +#include "pool.h" +#include "indexer.h" + +#include "git2/oid.h" +#include "git2/pack.h" + +#define GIT_PACK_WINDOW 10 /* number of objects to possibly delta against */ +#define GIT_PACK_DEPTH 50 /* max delta depth */ +#define GIT_PACK_DELTA_CACHE_SIZE (256 * 1024 * 1024) +#define GIT_PACK_DELTA_CACHE_LIMIT 1000 +#define GIT_PACK_BIG_FILE_THRESHOLD (512 * 1024 * 1024) + +typedef struct git_pobject { + git_oid id; + git_object_t type; + off64_t offset; + + size_t size; + + unsigned int hash; /* name hint hash */ + + struct git_pobject *delta; /* delta base object */ + struct git_pobject *delta_child; /* deltified objects who bases me */ + struct git_pobject *delta_sibling; /* other deltified objects + * who uses the same base as + * me */ + + void *delta_data; + size_t delta_size; + size_t z_delta_size; + + unsigned int written:1, + recursing:1, + tagged:1, + filled:1; +} git_pobject; + +struct git_packbuilder { + git_repository *repo; /* associated repository */ + git_odb *odb; /* associated object database */ + + git_oid_t oid_type; + + git_hash_ctx ctx; + git_zstream zstream; + + uint32_t nr_objects, + nr_deltified, + nr_written, + nr_remaining; + + size_t nr_alloc; + + git_pobject *object_list; + + git_oidmap *object_ix; + + git_oidmap *walk_objects; + git_pool object_pool; + +#ifndef GIT_DEPRECATE_HARD + git_oid pack_oid; /* hash of written pack */ +#endif + char *pack_name; /* name of written pack */ + + /* synchronization objects */ + git_mutex cache_mutex; + git_mutex progress_mutex; + git_cond progress_cond; + + /* configs */ + size_t delta_cache_size; + size_t max_delta_cache_size; + size_t cache_max_small_delta_size; + size_t big_file_threshold; + size_t window_memory_limit; + + unsigned int nr_threads; /* nr of threads to use */ + + git_packbuilder_progress progress_cb; + void *progress_cb_payload; + + /* the time progress was last reported, in millisecond ticks */ + uint64_t last_progress_report_time; + + bool done; +}; + +int git_packbuilder__write_buf(git_str *buf, git_packbuilder *pb); +int git_packbuilder__prepare(git_packbuilder *pb); + + +#endif diff --git a/src/libgit2/pack.c b/src/libgit2/pack.c new file mode 100644 index 0000000..eff7398 --- /dev/null +++ b/src/libgit2/pack.c @@ -0,0 +1,1658 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "pack.h" + +#include "delta.h" +#include "futils.h" +#include "mwindow.h" +#include "odb.h" +#include "oid.h" +#include "oidarray.h" + +/* Option to bypass checking existence of '.keep' files */ +bool git_disable_pack_keep_file_checks = false; + +static int packfile_open_locked(struct git_pack_file *p); +static off64_t nth_packed_object_offset_locked(struct git_pack_file *p, uint32_t n); +static int packfile_unpack_compressed( + git_rawobj *obj, + struct git_pack_file *p, + git_mwindow **w_curs, + off64_t *curpos, + size_t size, + git_object_t type); + +/* Can find the offset of an object given + * a prefix of an identifier. + * Throws GIT_EAMBIGUOUSOIDPREFIX if short oid + * is ambiguous within the pack. + * This method assumes that len is between + * GIT_OID_MINPREFIXLEN and the oid type's hexsize. + */ +static int pack_entry_find_offset( + off64_t *offset_out, + git_oid *found_oid, + struct git_pack_file *p, + const git_oid *short_oid, + size_t len); + +static int packfile_error(const char *message) +{ + git_error_set(GIT_ERROR_ODB, "invalid pack file - %s", message); + return -1; +} + +/******************** + * Delta base cache + ********************/ + +static git_pack_cache_entry *new_cache_object(git_rawobj *source) +{ + git_pack_cache_entry *e = git__calloc(1, sizeof(git_pack_cache_entry)); + if (!e) + return NULL; + + git_atomic32_inc(&e->refcount); + memcpy(&e->raw, source, sizeof(git_rawobj)); + + return e; +} + +static void free_cache_object(void *o) +{ + git_pack_cache_entry *e = (git_pack_cache_entry *)o; + + if (e != NULL) { + git__free(e->raw.data); + git__free(e); + } +} + +static void cache_free(git_pack_cache *cache) +{ + git_pack_cache_entry *entry; + + if (cache->entries) { + git_offmap_foreach_value(cache->entries, entry, { + free_cache_object(entry); + }); + + git_offmap_free(cache->entries); + cache->entries = NULL; + } +} + +static int cache_init(git_pack_cache *cache) +{ + if (git_offmap_new(&cache->entries) < 0) + return -1; + + cache->memory_limit = GIT_PACK_CACHE_MEMORY_LIMIT; + + if (git_mutex_init(&cache->lock)) { + git_error_set(GIT_ERROR_OS, "failed to initialize pack cache mutex"); + + git__free(cache->entries); + cache->entries = NULL; + + return -1; + } + + return 0; +} + +static git_pack_cache_entry *cache_get(git_pack_cache *cache, off64_t offset) +{ + git_pack_cache_entry *entry; + + if (git_mutex_lock(&cache->lock) < 0) + return NULL; + + if ((entry = git_offmap_get(cache->entries, offset)) != NULL) { + git_atomic32_inc(&entry->refcount); + entry->last_usage = cache->use_ctr++; + } + git_mutex_unlock(&cache->lock); + + return entry; +} + +/* Run with the cache lock held */ +static void free_lowest_entry(git_pack_cache *cache) +{ + off64_t offset; + git_pack_cache_entry *entry; + + git_offmap_foreach(cache->entries, offset, entry, { + if (entry && git_atomic32_get(&entry->refcount) == 0) { + cache->memory_used -= entry->raw.len; + git_offmap_delete(cache->entries, offset); + free_cache_object(entry); + } + }); +} + +static int cache_add( + git_pack_cache_entry **cached_out, + git_pack_cache *cache, + git_rawobj *base, + off64_t offset) +{ + git_pack_cache_entry *entry; + int exists; + + if (base->len > GIT_PACK_CACHE_SIZE_LIMIT) + return -1; + + entry = new_cache_object(base); + if (entry) { + if (git_mutex_lock(&cache->lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock cache"); + git__free(entry); + return -1; + } + /* Add it to the cache if nobody else has */ + exists = git_offmap_exists(cache->entries, offset); + if (!exists) { + while (cache->memory_used + base->len > cache->memory_limit) + free_lowest_entry(cache); + + git_offmap_set(cache->entries, offset, entry); + cache->memory_used += entry->raw.len; + + *cached_out = entry; + } + git_mutex_unlock(&cache->lock); + /* Somebody beat us to adding it into the cache */ + if (exists) { + git__free(entry); + return -1; + } + } + + return 0; +} + +/*********************************************************** + * + * PACK INDEX METHODS + * + ***********************************************************/ + +static void pack_index_free(struct git_pack_file *p) +{ + if (p->ids) { + git__free(p->ids); + p->ids = NULL; + } + if (p->index_map.data) { + git_futils_mmap_free(&p->index_map); + p->index_map.data = NULL; + } +} + +/* Run with the packfile lock held */ +static int pack_index_check_locked(const char *path, struct git_pack_file *p) +{ + struct git_pack_idx_header *hdr; + uint32_t version, nr = 0, i, *index; + void *idx_map; + size_t idx_size; + struct stat st; + int error; + + /* TODO: properly open the file without access time using O_NOATIME */ + git_file fd = git_futils_open_ro(path); + if (fd < 0) + return fd; + + if (p_fstat(fd, &st) < 0) { + p_close(fd); + git_error_set(GIT_ERROR_OS, "unable to stat pack index '%s'", path); + return -1; + } + + if (!S_ISREG(st.st_mode) || + !git__is_sizet(st.st_size) || + (idx_size = (size_t)st.st_size) < (size_t)((4 * 256) + (p->oid_size * 2))) { + p_close(fd); + git_error_set(GIT_ERROR_ODB, "invalid pack index '%s'", path); + return -1; + } + + error = git_futils_mmap_ro(&p->index_map, fd, 0, idx_size); + + p_close(fd); + + if (error < 0) + return error; + + hdr = idx_map = p->index_map.data; + + if (hdr->idx_signature == htonl(PACK_IDX_SIGNATURE)) { + version = ntohl(hdr->idx_version); + + if (version < 2 || version > 2) { + git_futils_mmap_free(&p->index_map); + return packfile_error("unsupported index version"); + } + + } else { + version = 1; + } + + index = idx_map; + + if (version > 1) + index += 2; /* skip index header */ + + for (i = 0; i < 256; i++) { + uint32_t n = ntohl(index[i]); + if (n < nr) { + git_futils_mmap_free(&p->index_map); + return packfile_error("index is non-monotonic"); + } + nr = n; + } + + if (version == 1) { + /* + * Total size: + * - 256 index entries 4 bytes each + * - 24/36-byte entries * nr (20/32 byte SHA + 4-byte offset) + * - 20/32-byte SHA of the packfile + * - 20/32-byte SHA file checksum + */ + if (idx_size != (4 * 256 + ((uint64_t) nr * (p->oid_size + 4)) + (p->oid_size * 2))) { + git_futils_mmap_free(&p->index_map); + return packfile_error("index is corrupted"); + } + } else if (version == 2) { + /* + * Minimum size: + * - 8 bytes of header + * - 256 index entries 4 bytes each + * - 20/32-byte SHA entry * nr + * - 4-byte crc entry * nr + * - 4-byte offset entry * nr + * - 20/32-byte SHA of the packfile + * - 20/32-byte SHA file checksum + * And after the 4-byte offset table might be a + * variable sized table containing 8-byte entries + * for offsets larger than 2^31. + */ + uint64_t min_size = 8 + (4 * 256) + ((uint64_t)nr * (p->oid_size + 4 + 4)) + (p->oid_size * 2); + uint64_t max_size = min_size; + + if (nr) + max_size += (nr - 1)*8; + + if (idx_size < min_size || idx_size > max_size) { + git_futils_mmap_free(&p->index_map); + return packfile_error("wrong index size"); + } + } + + p->num_objects = nr; + p->index_version = version; + return 0; +} + +/* Run with the packfile lock held */ +static int pack_index_open_locked(struct git_pack_file *p) +{ + int error = 0; + size_t name_len; + git_str idx_name = GIT_STR_INIT; + + if (p->index_version > -1) + goto cleanup; + + /* checked by git_pack_file alloc */ + name_len = strlen(p->pack_name); + GIT_ASSERT(name_len > strlen(".pack")); + + if ((error = git_str_init(&idx_name, name_len)) < 0) + goto cleanup; + + git_str_put(&idx_name, p->pack_name, name_len - strlen(".pack")); + git_str_puts(&idx_name, ".idx"); + if (git_str_oom(&idx_name)) { + error = -1; + goto cleanup; + } + + if (p->index_version == -1) + error = pack_index_check_locked(idx_name.ptr, p); + +cleanup: + git_str_dispose(&idx_name); + + return error; +} + +static unsigned char *pack_window_open( + struct git_pack_file *p, + git_mwindow **w_cursor, + off64_t offset, + unsigned int *left) +{ + unsigned char *pack_data = NULL; + + if (git_mutex_lock(&p->lock) < 0) { + git_error_set(GIT_ERROR_THREAD, "unable to lock packfile"); + return NULL; + } + if (git_mutex_lock(&p->mwf.lock) < 0) { + git_mutex_unlock(&p->lock); + git_error_set(GIT_ERROR_THREAD, "unable to lock packfile"); + return NULL; + } + + if (p->mwf.fd == -1 && packfile_open_locked(p) < 0) + goto cleanup; + + /* Since packfiles end in a hash of their content and it's + * pointless to ask for an offset into the middle of that + * hash, and the pack_window_contains function above wouldn't match + * don't allow an offset too close to the end of the file. + * + * Don't allow a negative offset, as that means we've wrapped + * around. + */ + if (offset > (p->mwf.size - p->oid_size)) + goto cleanup; + if (offset < 0) + goto cleanup; + + pack_data = git_mwindow_open(&p->mwf, w_cursor, offset, p->oid_size, left); + +cleanup: + git_mutex_unlock(&p->mwf.lock); + git_mutex_unlock(&p->lock); + return pack_data; + } + +/* + * The per-object header is a pretty dense thing, which is + * - first byte: low four bits are "size", + * then three bits of "type", + * with the high bit being "size continues". + * - each byte afterwards: low seven bits are size continuation, + * with the high bit being "size continues" + */ +int git_packfile__object_header(size_t *out, unsigned char *hdr, size_t size, git_object_t type) +{ + unsigned char *hdr_base; + unsigned char c; + + GIT_ASSERT_ARG(type >= GIT_OBJECT_COMMIT && type <= GIT_OBJECT_REF_DELTA); + + /* TODO: add support for chunked objects; see git.git 6c0d19b1 */ + + c = (unsigned char)((type << 4) | (size & 15)); + size >>= 4; + hdr_base = hdr; + + while (size) { + *hdr++ = c | 0x80; + c = size & 0x7f; + size >>= 7; + } + *hdr++ = c; + + *out = (hdr - hdr_base); + return 0; +} + + +static int packfile_unpack_header1( + unsigned long *usedp, + size_t *sizep, + git_object_t *type, + const unsigned char *buf, + unsigned long len) +{ + unsigned shift; + unsigned long size, c; + unsigned long used = 0; + + c = buf[used++]; + *type = (c >> 4) & 7; + size = c & 15; + shift = 4; + while (c & 0x80) { + if (len <= used) { + git_error_set(GIT_ERROR_ODB, "buffer too small"); + return GIT_EBUFS; + } + + if (bitsizeof(long) <= shift) { + *usedp = 0; + git_error_set(GIT_ERROR_ODB, "packfile corrupted"); + return -1; + } + + c = buf[used++]; + size += (c & 0x7f) << shift; + shift += 7; + } + + *sizep = (size_t)size; + *usedp = used; + return 0; +} + +int git_packfile_unpack_header( + size_t *size_p, + git_object_t *type_p, + struct git_pack_file *p, + git_mwindow **w_curs, + off64_t *curpos) +{ + unsigned char *base; + unsigned int left; + unsigned long used; + int error; + + if ((error = git_mutex_lock(&p->lock)) < 0) + return error; + if ((error = git_mutex_lock(&p->mwf.lock)) < 0) { + git_mutex_unlock(&p->lock); + return error; + } + + if (p->mwf.fd == -1 && (error = packfile_open_locked(p)) < 0) { + git_mutex_unlock(&p->lock); + git_mutex_unlock(&p->mwf.lock); + return error; + } + + /* pack_window_open() assures us we have [base, base + oid_size) + * available as a range that we can look at at. (It's actually + * the hash size that is assured.) With our object header + * encoding the maximum deflated object size is 2^137, which is + * just insane, so we know won't exceed what we have been given. + */ + base = git_mwindow_open(&p->mwf, w_curs, *curpos, p->oid_size, &left); + git_mutex_unlock(&p->lock); + git_mutex_unlock(&p->mwf.lock); + if (base == NULL) + return GIT_EBUFS; + + error = packfile_unpack_header1(&used, size_p, type_p, base, left); + git_mwindow_close(w_curs); + if (error == GIT_EBUFS) + return error; + else if (error < 0) + return packfile_error("header length is zero"); + + *curpos += used; + return 0; +} + +int git_packfile_resolve_header( + size_t *size_p, + git_object_t *type_p, + struct git_pack_file *p, + off64_t offset) +{ + git_mwindow *w_curs = NULL; + off64_t curpos = offset; + size_t size; + git_object_t type; + off64_t base_offset; + int error; + + error = git_mutex_lock(&p->lock); + if (error < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock packfile reader"); + return error; + } + error = git_mutex_lock(&p->mwf.lock); + if (error < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock packfile reader"); + git_mutex_unlock(&p->lock); + return error; + } + + if (p->mwf.fd == -1 && (error = packfile_open_locked(p)) < 0) { + git_mutex_unlock(&p->mwf.lock); + git_mutex_unlock(&p->lock); + return error; + } + git_mutex_unlock(&p->mwf.lock); + git_mutex_unlock(&p->lock); + + error = git_packfile_unpack_header(&size, &type, p, &w_curs, &curpos); + if (error < 0) + return error; + + if (type == GIT_OBJECT_OFS_DELTA || type == GIT_OBJECT_REF_DELTA) { + size_t base_size; + git_packfile_stream stream; + + error = get_delta_base(&base_offset, p, &w_curs, &curpos, type, offset); + git_mwindow_close(&w_curs); + + if (error < 0) + return error; + + if ((error = git_packfile_stream_open(&stream, p, curpos)) < 0) + return error; + error = git_delta_read_header_fromstream(&base_size, size_p, &stream); + git_packfile_stream_dispose(&stream); + if (error < 0) + return error; + } else { + *size_p = size; + base_offset = 0; + } + + while (type == GIT_OBJECT_OFS_DELTA || type == GIT_OBJECT_REF_DELTA) { + curpos = base_offset; + error = git_packfile_unpack_header(&size, &type, p, &w_curs, &curpos); + if (error < 0) + return error; + if (type != GIT_OBJECT_OFS_DELTA && type != GIT_OBJECT_REF_DELTA) + break; + + error = get_delta_base(&base_offset, p, &w_curs, &curpos, type, base_offset); + git_mwindow_close(&w_curs); + + if (error < 0) + return error; + } + *type_p = type; + + return error; +} + +#define SMALL_STACK_SIZE 64 + +/** + * Generate the chain of dependencies which we need to get to the + * object at `off`. `chain` is used a stack, popping gives the right + * order to apply deltas on. If an object is found in the pack's base + * cache, we stop calculating there. + */ +static int pack_dependency_chain(git_dependency_chain *chain_out, + git_pack_cache_entry **cached_out, off64_t *cached_off, + struct pack_chain_elem *small_stack, size_t *stack_sz, + struct git_pack_file *p, off64_t obj_offset) +{ + git_dependency_chain chain = GIT_ARRAY_INIT; + git_mwindow *w_curs = NULL; + off64_t curpos = obj_offset, base_offset; + int error = 0, use_heap = 0; + size_t size, elem_pos; + git_object_t type; + + elem_pos = 0; + while (true) { + struct pack_chain_elem *elem; + git_pack_cache_entry *cached = NULL; + + /* if we have a base cached, we can stop here instead */ + if ((cached = cache_get(&p->bases, obj_offset)) != NULL) { + *cached_out = cached; + *cached_off = obj_offset; + break; + } + + /* if we run out of space on the small stack, use the array */ + if (elem_pos == SMALL_STACK_SIZE) { + git_array_init_to_size(chain, elem_pos); + GIT_ERROR_CHECK_ARRAY(chain); + memcpy(chain.ptr, small_stack, elem_pos * sizeof(struct pack_chain_elem)); + chain.size = elem_pos; + use_heap = 1; + } + + curpos = obj_offset; + if (!use_heap) { + elem = &small_stack[elem_pos]; + } else { + elem = git_array_alloc(chain); + if (!elem) { + error = -1; + goto on_error; + } + } + + elem->base_key = obj_offset; + + error = git_packfile_unpack_header(&size, &type, p, &w_curs, &curpos); + if (error < 0) + goto on_error; + + elem->offset = curpos; + elem->size = size; + elem->type = type; + elem->base_key = obj_offset; + + if (type != GIT_OBJECT_OFS_DELTA && type != GIT_OBJECT_REF_DELTA) + break; + + error = get_delta_base(&base_offset, p, &w_curs, &curpos, type, obj_offset); + git_mwindow_close(&w_curs); + + if (error < 0) + goto on_error; + + /* we need to pass the pos *after* the delta-base bit */ + elem->offset = curpos; + + /* go through the loop again, but with the new object */ + obj_offset = base_offset; + elem_pos++; + } + + + *stack_sz = elem_pos + 1; + *chain_out = chain; + return error; + +on_error: + git_array_clear(chain); + return error; +} + +int git_packfile_unpack( + git_rawobj *obj, + struct git_pack_file *p, + off64_t *obj_offset) +{ + git_mwindow *w_curs = NULL; + off64_t curpos = *obj_offset; + int error, free_base = 0; + git_dependency_chain chain = GIT_ARRAY_INIT; + struct pack_chain_elem *elem = NULL, *stack; + git_pack_cache_entry *cached = NULL; + struct pack_chain_elem small_stack[SMALL_STACK_SIZE]; + size_t stack_size = 0, elem_pos, alloclen; + git_object_t base_type; + + error = git_mutex_lock(&p->lock); + if (error < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock packfile reader"); + return error; + } + error = git_mutex_lock(&p->mwf.lock); + if (error < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock packfile reader"); + git_mutex_unlock(&p->lock); + return error; + } + + if (p->mwf.fd == -1) + error = packfile_open_locked(p); + git_mutex_unlock(&p->mwf.lock); + git_mutex_unlock(&p->lock); + if (error < 0) + return error; + + /* + * TODO: optionally check the CRC on the packfile + */ + + error = pack_dependency_chain(&chain, &cached, obj_offset, small_stack, &stack_size, p, *obj_offset); + if (error < 0) + return error; + + obj->data = NULL; + obj->len = 0; + obj->type = GIT_OBJECT_INVALID; + + /* let's point to the right stack */ + stack = chain.ptr ? chain.ptr : small_stack; + + elem_pos = stack_size; + if (cached) { + memcpy(obj, &cached->raw, sizeof(git_rawobj)); + base_type = obj->type; + elem_pos--; /* stack_size includes the base, which isn't actually there */ + } else { + elem = &stack[--elem_pos]; + base_type = elem->type; + } + + switch (base_type) { + case GIT_OBJECT_COMMIT: + case GIT_OBJECT_TREE: + case GIT_OBJECT_BLOB: + case GIT_OBJECT_TAG: + if (!cached) { + curpos = elem->offset; + error = packfile_unpack_compressed(obj, p, &w_curs, &curpos, elem->size, elem->type); + git_mwindow_close(&w_curs); + base_type = elem->type; + } + if (error < 0) + goto cleanup; + break; + case GIT_OBJECT_OFS_DELTA: + case GIT_OBJECT_REF_DELTA: + error = packfile_error("dependency chain ends in a delta"); + goto cleanup; + default: + error = packfile_error("invalid packfile type in header"); + goto cleanup; + } + + /* + * Finding the object we want a cached base element is + * problematic, as we need to make sure we don't accidentally + * give the caller the cached object, which it would then feel + * free to free, so we need to copy the data. + */ + if (cached && stack_size == 1) { + void *data = obj->data; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, obj->len, 1); + obj->data = git__malloc(alloclen); + GIT_ERROR_CHECK_ALLOC(obj->data); + + memcpy(obj->data, data, obj->len + 1); + git_atomic32_dec(&cached->refcount); + goto cleanup; + } + + /* we now apply each consecutive delta until we run out */ + while (elem_pos > 0 && !error) { + git_rawobj base, delta; + + /* + * We can now try to add the base to the cache, as + * long as it's not already the cached one. + */ + if (!cached) + free_base = !!cache_add(&cached, &p->bases, obj, elem->base_key); + + elem = &stack[elem_pos - 1]; + curpos = elem->offset; + error = packfile_unpack_compressed(&delta, p, &w_curs, &curpos, elem->size, elem->type); + git_mwindow_close(&w_curs); + + if (error < 0) { + /* We have transferred ownership of the data to the cache. */ + obj->data = NULL; + break; + } + + /* the current object becomes the new base, on which we apply the delta */ + base = *obj; + obj->data = NULL; + obj->len = 0; + obj->type = GIT_OBJECT_INVALID; + + error = git_delta_apply(&obj->data, &obj->len, base.data, base.len, delta.data, delta.len); + obj->type = base_type; + + /* + * We usually don't want to free the base at this + * point, as we put it into the cache in the previous + * iteration. free_base lets us know that we got the + * base object directly from the packfile, so we can free it. + */ + git__free(delta.data); + if (free_base) { + free_base = 0; + git__free(base.data); + } + + if (cached) { + git_atomic32_dec(&cached->refcount); + cached = NULL; + } + + if (error < 0) + break; + + elem_pos--; + } + +cleanup: + if (error < 0) { + git__free(obj->data); + if (cached) + git_atomic32_dec(&cached->refcount); + } + + if (elem) + *obj_offset = curpos; + + git_array_clear(chain); + return error; +} + +int git_packfile_stream_open(git_packfile_stream *obj, struct git_pack_file *p, off64_t curpos) +{ + memset(obj, 0, sizeof(git_packfile_stream)); + obj->curpos = curpos; + obj->p = p; + + if (git_zstream_init(&obj->zstream, GIT_ZSTREAM_INFLATE) < 0) { + git_error_set(GIT_ERROR_ZLIB, "failed to init packfile stream"); + return -1; + } + + return 0; +} + +ssize_t git_packfile_stream_read(git_packfile_stream *obj, void *buffer, size_t len) +{ + unsigned int window_len; + unsigned char *in; + int error; + + if (obj->done) + return 0; + + if ((in = pack_window_open(obj->p, &obj->mw, obj->curpos, &window_len)) == NULL) + return GIT_EBUFS; + + if ((error = git_zstream_set_input(&obj->zstream, in, window_len)) < 0 || + (error = git_zstream_get_output_chunk(buffer, &len, &obj->zstream)) < 0) { + git_mwindow_close(&obj->mw); + git_error_set(GIT_ERROR_ZLIB, "error reading from the zlib stream"); + return -1; + } + + git_mwindow_close(&obj->mw); + + obj->curpos += window_len - obj->zstream.in_len; + + if (git_zstream_eos(&obj->zstream)) + obj->done = 1; + + /* If we didn't write anything out but we're not done, we need more data */ + if (!len && !git_zstream_eos(&obj->zstream)) + return GIT_EBUFS; + + return len; + +} + +void git_packfile_stream_dispose(git_packfile_stream *obj) +{ + git_zstream_free(&obj->zstream); +} + +static int packfile_unpack_compressed( + git_rawobj *obj, + struct git_pack_file *p, + git_mwindow **mwindow, + off64_t *position, + size_t size, + git_object_t type) +{ + git_zstream zstream = GIT_ZSTREAM_INIT; + size_t buffer_len, total = 0; + char *data = NULL; + int error; + + GIT_ERROR_CHECK_ALLOC_ADD(&buffer_len, size, 1); + data = git__calloc(1, buffer_len); + GIT_ERROR_CHECK_ALLOC(data); + + if ((error = git_zstream_init(&zstream, GIT_ZSTREAM_INFLATE)) < 0) { + git_error_set(GIT_ERROR_ZLIB, "failed to init zlib stream on unpack"); + goto out; + } + + do { + size_t bytes = buffer_len - total; + unsigned int window_len, consumed; + unsigned char *in; + + if ((in = pack_window_open(p, mwindow, *position, &window_len)) == NULL) { + error = -1; + goto out; + } + + if ((error = git_zstream_set_input(&zstream, in, window_len)) < 0 || + (error = git_zstream_get_output_chunk(data + total, &bytes, &zstream)) < 0) { + git_mwindow_close(mwindow); + goto out; + } + + git_mwindow_close(mwindow); + + consumed = window_len - (unsigned int)zstream.in_len; + + if (!bytes && !consumed) { + git_error_set(GIT_ERROR_ZLIB, "error inflating zlib stream"); + error = -1; + goto out; + } + + *position += consumed; + total += bytes; + } while (!git_zstream_eos(&zstream)); + + if (total != size || !git_zstream_eos(&zstream)) { + git_error_set(GIT_ERROR_ZLIB, "error inflating zlib stream"); + error = -1; + goto out; + } + + obj->type = type; + obj->len = size; + obj->data = data; + +out: + git_zstream_free(&zstream); + if (error) + git__free(data); + + return error; +} + +/* + * curpos is where the data starts, delta_obj_offset is the where the + * header starts + */ +int get_delta_base( + off64_t *delta_base_out, + struct git_pack_file *p, + git_mwindow **w_curs, + off64_t *curpos, + git_object_t type, + off64_t delta_obj_offset) +{ + unsigned int left = 0; + unsigned char *base_info; + off64_t base_offset; + git_oid unused; + + GIT_ASSERT_ARG(delta_base_out); + + base_info = pack_window_open(p, w_curs, *curpos, &left); + /* Assumption: the only reason this would fail is because the file is too small */ + if (base_info == NULL) + return GIT_EBUFS; + /* pack_window_open() assured us we have + * [base_info, base_info + oid_size) as a range that we can look + * at without walking off the end of the mapped window. Its + * actually the hash size that is assured. An OFS_DELTA longer + * than the hash size is stupid, as then a REF_DELTA would be + * smaller to store. + */ + if (type == GIT_OBJECT_OFS_DELTA) { + unsigned used = 0; + unsigned char c = base_info[used++]; + size_t unsigned_base_offset = c & 127; + while (c & 128) { + if (left <= used) + return GIT_EBUFS; + unsigned_base_offset += 1; + if (!unsigned_base_offset || MSB(unsigned_base_offset, 7)) + return packfile_error("overflow"); + c = base_info[used++]; + unsigned_base_offset = (unsigned_base_offset << 7) + (c & 127); + } + if (unsigned_base_offset == 0 || (size_t)delta_obj_offset <= unsigned_base_offset) + return packfile_error("out of bounds"); + base_offset = delta_obj_offset - unsigned_base_offset; + *curpos += used; + } else if (type == GIT_OBJECT_REF_DELTA) { + git_oid base_oid; + git_oid__fromraw(&base_oid, base_info, p->oid_type); + + /* If we have the cooperative cache, search in it first */ + if (p->has_cache) { + struct git_pack_entry *entry; + + if ((entry = git_oidmap_get(p->idx_cache, &base_oid)) != NULL) { + if (entry->offset == 0) + return packfile_error("delta offset is zero"); + + *curpos += p->oid_size; + *delta_base_out = entry->offset; + return 0; + } else { + /* If we're building an index, don't try to find the pack + * entry; we just haven't seen it yet. We'll make + * progress again in the next loop. + */ + return GIT_PASSTHROUGH; + } + } + + /* The base entry _must_ be in the same pack */ + if (pack_entry_find_offset(&base_offset, &unused, p, &base_oid, p->oid_hexsize) < 0) + return packfile_error("base entry delta is not in the same pack"); + *curpos += p->oid_size; + } else + return packfile_error("unknown object type"); + + if (base_offset == 0) + return packfile_error("delta offset is zero"); + + *delta_base_out = base_offset; + return 0; +} + +/*********************************************************** + * + * PACKFILE METHODS + * + ***********************************************************/ + +void git_packfile_free(struct git_pack_file *p, bool unlink_packfile) +{ + bool locked = true; + + if (!p) + return; + + cache_free(&p->bases); + + if (git_mutex_lock(&p->lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock packfile"); + locked = false; + } + if (p->mwf.fd >= 0) { + git_mwindow_free_all(&p->mwf); + p_close(p->mwf.fd); + p->mwf.fd = -1; + } + if (locked) + git_mutex_unlock(&p->lock); + + if (unlink_packfile) + p_unlink(p->pack_name); + + pack_index_free(p); + + git__free(p->bad_object_ids); + + git_mutex_free(&p->bases.lock); + git_mutex_free(&p->mwf.lock); + git_mutex_free(&p->lock); + git__free(p); +} + +/* Run with the packfile and mwf locks held */ +static int packfile_open_locked(struct git_pack_file *p) +{ + struct stat st; + struct git_pack_header hdr; + unsigned char checksum[GIT_OID_MAX_SIZE]; + unsigned char *idx_checksum; + + if (pack_index_open_locked(p) < 0) + return git_odb__error_notfound("failed to open packfile", NULL, 0); + + if (p->mwf.fd >= 0) + return 0; + + /* TODO: open with noatime */ + p->mwf.fd = git_futils_open_ro(p->pack_name); + if (p->mwf.fd < 0) + goto cleanup; + + if (p_fstat(p->mwf.fd, &st) < 0) { + git_error_set(GIT_ERROR_OS, "could not stat packfile"); + goto cleanup; + } + + /* If we created the struct before we had the pack we lack size. */ + if (!p->mwf.size) { + if (!S_ISREG(st.st_mode)) + goto cleanup; + p->mwf.size = (off64_t)st.st_size; + } else if (p->mwf.size != st.st_size) + goto cleanup; + +#if 0 + /* We leave these file descriptors open with sliding mmap; + * there is no point keeping them open across exec(), though. + */ + fd_flag = fcntl(p->mwf.fd, F_GETFD, 0); + if (fd_flag < 0) + goto cleanup; + + fd_flag |= FD_CLOEXEC; + if (fcntl(p->pack_fd, F_SETFD, fd_flag) == -1) + goto cleanup; +#endif + + /* Verify we recognize this pack file format. */ + if (p_read(p->mwf.fd, &hdr, sizeof(hdr)) < 0 || + hdr.hdr_signature != htonl(PACK_SIGNATURE) || + !pack_version_ok(hdr.hdr_version)) + goto cleanup; + + /* Verify the pack matches its index. */ + if (p->num_objects != ntohl(hdr.hdr_entries) || + p_pread(p->mwf.fd, checksum, p->oid_size, p->mwf.size - p->oid_size) < 0) + goto cleanup; + + idx_checksum = ((unsigned char *)p->index_map.data) + + p->index_map.len - (p->oid_size * 2); + + if (git_oid_raw_cmp(checksum, idx_checksum, p->oid_size) != 0) + goto cleanup; + + if (git_mwindow_file_register(&p->mwf) < 0) + goto cleanup; + + return 0; + +cleanup: + git_error_set(GIT_ERROR_OS, "invalid packfile '%s'", p->pack_name); + + if (p->mwf.fd >= 0) + p_close(p->mwf.fd); + p->mwf.fd = -1; + + return -1; +} + +int git_packfile__name(char **out, const char *path) +{ + size_t path_len; + git_str buf = GIT_STR_INIT; + + path_len = strlen(path); + + if (path_len < strlen(".idx")) + return git_odb__error_notfound("invalid packfile path", NULL, 0); + + if (git_str_printf(&buf, "%.*s.pack", (int)(path_len - strlen(".idx")), path) < 0) + return -1; + + *out = git_str_detach(&buf); + return 0; +} + +int git_packfile_alloc( + struct git_pack_file **pack_out, + const char *path, + git_oid_t oid_type) +{ + struct stat st; + struct git_pack_file *p; + size_t path_len = path ? strlen(path) : 0, alloc_len; + + *pack_out = NULL; + + if (path_len < strlen(".idx")) + return git_odb__error_notfound("invalid packfile path", NULL, 0); + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, sizeof(*p), path_len); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, 2); + + p = git__calloc(1, alloc_len); + GIT_ERROR_CHECK_ALLOC(p); + + memcpy(p->pack_name, path, path_len + 1); + + /* + * Make sure a corresponding .pack file exists and that + * the index looks sane. + */ + if (git__suffixcmp(path, ".idx") == 0) { + size_t root_len = path_len - strlen(".idx"); + + if (!git_disable_pack_keep_file_checks) { + memcpy(p->pack_name + root_len, ".keep", sizeof(".keep")); + if (git_fs_path_exists(p->pack_name) == true) + p->pack_keep = 1; + } + + memcpy(p->pack_name + root_len, ".pack", sizeof(".pack")); + } + + if (p_stat(p->pack_name, &st) < 0 || !S_ISREG(st.st_mode)) { + git__free(p); + return git_odb__error_notfound("packfile not found", NULL, 0); + } + + /* ok, it looks sane as far as we can check without + * actually mapping the pack file. + */ + p->mwf.fd = -1; + p->mwf.size = st.st_size; + p->pack_local = 1; + p->mtime = (git_time_t)st.st_mtime; + p->index_version = -1; + p->oid_type = oid_type ? oid_type : GIT_OID_DEFAULT; + p->oid_size = (unsigned int)git_oid_size(p->oid_type); + p->oid_hexsize = (unsigned int)git_oid_hexsize(p->oid_type); + + if (git_mutex_init(&p->lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to initialize packfile mutex"); + git__free(p); + return -1; + } + + if (git_mutex_init(&p->mwf.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to initialize packfile window mutex"); + git_mutex_free(&p->lock); + git__free(p); + return -1; + } + + if (cache_init(&p->bases) < 0) { + git_mutex_free(&p->mwf.lock); + git_mutex_free(&p->lock); + git__free(p); + return -1; + } + + *pack_out = p; + + return 0; +} + +/*********************************************************** + * + * PACKFILE ENTRY SEARCH INTERNALS + * + ***********************************************************/ + +static off64_t nth_packed_object_offset_locked(struct git_pack_file *p, uint32_t n) +{ + const unsigned char *index, *end; + uint32_t off32; + + index = p->index_map.data; + end = index + p->index_map.len; + index += 4 * 256; + if (p->index_version == 1) + return ntohl(*((uint32_t *)(index + (p->oid_size + 4) * (size_t) n))); + + index += 8 + (size_t) p->num_objects * (p->oid_size + 4); + off32 = ntohl(*((uint32_t *)(index + 4 * n))); + if (!(off32 & 0x80000000)) + return off32; + index += (size_t) p->num_objects * 4 + (off32 & 0x7fffffff) * 8; + + /* Make sure we're not being sent out of bounds */ + if (index >= end - 8) + return -1; + + return (((uint64_t)ntohl(*((uint32_t *)(index + 0)))) << 32) | + ntohl(*((uint32_t *)(index + 4))); +} + +static int git__memcmp4(const void *a, const void *b) { + return memcmp(a, b, 4); +} + +int git_pack_foreach_entry( + struct git_pack_file *p, + git_odb_foreach_cb cb, + void *data) +{ + const unsigned char *index, *current; + uint32_t i; + int error = 0; + git_array_oid_t oids = GIT_ARRAY_INIT; + git_oid *oid; + + if (git_mutex_lock(&p->lock) < 0) + return packfile_error("failed to get lock for git_pack_foreach_entry"); + + if ((error = pack_index_open_locked(p)) < 0) { + git_mutex_unlock(&p->lock); + return error; + } + + if (!p->index_map.data) { + git_error_set(GIT_ERROR_INTERNAL, "internal error: p->index_map.data == NULL"); + git_mutex_unlock(&p->lock); + return -1; + } + + index = p->index_map.data; + + if (p->index_version > 1) + index += 8; + + index += 4 * 256; + + if (p->ids == NULL) { + git_vector offsets, oids; + + if ((error = git_vector_init(&oids, p->num_objects, NULL))) { + git_mutex_unlock(&p->lock); + return error; + } + + if ((error = git_vector_init(&offsets, p->num_objects, git__memcmp4))) { + git_mutex_unlock(&p->lock); + return error; + } + + if (p->index_version > 1) { + const unsigned char *off = index + + (p->oid_size + 4) * p->num_objects; + + for (i = 0; i < p->num_objects; i++) + git_vector_insert(&offsets, (void*)&off[4 * i]); + + git_vector_sort(&offsets); + git_vector_foreach(&offsets, i, current) + git_vector_insert(&oids, (void*)&index[5 * (current - off)]); + } else { + for (i = 0; i < p->num_objects; i++) + git_vector_insert(&offsets, (void*)&index[(p->oid_size + 4) * i]); + git_vector_sort(&offsets); + git_vector_foreach(&offsets, i, current) + git_vector_insert(&oids, (void*)¤t[4]); + } + + git_vector_free(&offsets); + p->ids = (unsigned char **)git_vector_detach(NULL, NULL, &oids); + } + + /* + * We need to copy the OIDs to another array before we + * relinquish the lock to avoid races. We can also take + * this opportunity to put them into normal form. + */ + git_array_init_to_size(oids, p->num_objects); + if (!oids.ptr) { + git_mutex_unlock(&p->lock); + git_array_clear(oids); + GIT_ERROR_CHECK_ARRAY(oids); + } + for (i = 0; i < p->num_objects; i++) { + oid = git_array_alloc(oids); + if (!oid) { + git_mutex_unlock(&p->lock); + git_array_clear(oids); + GIT_ERROR_CHECK_ALLOC(oid); + } + git_oid__fromraw(oid, p->ids[i], p->oid_type); + } + + git_mutex_unlock(&p->lock); + + git_array_foreach(oids, i, oid) { + if ((error = cb(oid, data)) != 0) { + git_error_set_after_callback(error); + break; + } + } + + git_array_clear(oids); + return error; +} + +int git_pack_foreach_entry_offset( + struct git_pack_file *p, + git_pack_foreach_entry_offset_cb cb, + void *data) +{ + const unsigned char *index; + off64_t current_offset; + git_oid current_oid; + uint32_t i; + int error = 0; + + if (git_mutex_lock(&p->lock) < 0) + return packfile_error("failed to get lock for git_pack_foreach_entry_offset"); + + index = p->index_map.data; + if (index == NULL) { + if ((error = pack_index_open_locked(p)) < 0) + goto cleanup; + + if (!p->index_map.data) { + git_error_set(GIT_ERROR_INTERNAL, "internal error: p->index_map.data == NULL"); + goto cleanup; + } + + index = p->index_map.data; + } + + if (p->index_version > 1) + index += 8; + + index += 4 * 256; + + /* all offsets should have been validated by pack_index_check_locked */ + if (p->index_version > 1) { + const unsigned char *offsets = index + + (p->oid_size + 4) * p->num_objects; + const unsigned char *large_offset_ptr; + const unsigned char *large_offsets = index + + (p->oid_size + 8) * p->num_objects; + const unsigned char *large_offsets_end = ((const unsigned char *)p->index_map.data) + p->index_map.len - p->oid_size; + + for (i = 0; i < p->num_objects; i++) { + current_offset = ntohl(*(const uint32_t *)(offsets + 4 * i)); + if (current_offset & 0x80000000) { + large_offset_ptr = large_offsets + (current_offset & 0x7fffffff) * 8; + if (large_offset_ptr >= large_offsets_end) { + error = packfile_error("invalid large offset"); + goto cleanup; + } + current_offset = (((off64_t)ntohl(*((uint32_t *)(large_offset_ptr + 0)))) << 32) | + ntohl(*((uint32_t *)(large_offset_ptr + 4))); + } + + git_oid__fromraw(¤t_oid, (index + p->oid_size * i), p->oid_type); + if ((error = cb(¤t_oid, current_offset, data)) != 0) { + error = git_error_set_after_callback(error); + goto cleanup; + } + } + } else { + for (i = 0; i < p->num_objects; i++) { + current_offset = ntohl(*(const uint32_t *)(index + (p->oid_size + 4) * i)); + git_oid__fromraw(¤t_oid, (index + (p->oid_size + 4) * i + 4), p->oid_type); + if ((error = cb(¤t_oid, current_offset, data)) != 0) { + error = git_error_set_after_callback(error); + goto cleanup; + } + } + } + +cleanup: + git_mutex_unlock(&p->lock); + return error; +} + +int git_pack__lookup_id( + const void *oid_lookup_table, + size_t stride, + unsigned lo, + unsigned hi, + const unsigned char *oid_prefix, + const git_oid_t oid_type) +{ + const unsigned char *base = oid_lookup_table; + size_t oid_size = git_oid_size(oid_type); + + while (lo < hi) { + unsigned mi = (lo + hi) / 2; + int cmp = git_oid_raw_cmp(base + mi * stride, oid_prefix, oid_size); + + if (!cmp) + return mi; + + if (cmp > 0) + hi = mi; + else + lo = mi+1; + } + + return -((int)lo)-1; +} + +static int pack_entry_find_offset( + off64_t *offset_out, + git_oid *found_oid, + struct git_pack_file *p, + const git_oid *short_oid, + size_t len) +{ + const uint32_t *level1_ofs; + const unsigned char *index; + unsigned hi, lo, stride; + int pos, found = 0; + off64_t offset; + const unsigned char *current = 0; + int error = 0; + + *offset_out = 0; + + if (git_mutex_lock(&p->lock) < 0) + return packfile_error("failed to get lock for pack_entry_find_offset"); + + if ((error = pack_index_open_locked(p)) < 0) + goto cleanup; + + if (!p->index_map.data) { + git_error_set(GIT_ERROR_INTERNAL, "internal error: p->index_map.data == NULL"); + goto cleanup; + } + + index = p->index_map.data; + level1_ofs = p->index_map.data; + + if (p->index_version > 1) { + level1_ofs += 2; + index += 8; + } + + index += 4 * 256; + hi = ntohl(level1_ofs[(int)short_oid->id[0]]); + lo = ((short_oid->id[0] == 0x0) ? 0 : ntohl(level1_ofs[(int)short_oid->id[0] - 1])); + + if (p->index_version > 1) { + stride = p->oid_size; + } else { + stride = p->oid_size + 4; + index += 4; + } + +#ifdef INDEX_DEBUG_LOOKUP + printf("%02x%02x%02x... lo %u hi %u nr %d\n", + short_oid->id[0], short_oid->id[1], short_oid->id[2], lo, hi, p->num_objects); +#endif + + pos = git_pack__lookup_id(index, stride, lo, hi, + short_oid->id, p->oid_type); + + if (pos >= 0) { + /* An object matching exactly the oid was found */ + found = 1; + current = index + pos * stride; + } else { + /* No object was found */ + /* pos refers to the object with the "closest" oid to short_oid */ + pos = - 1 - pos; + if (pos < (int)p->num_objects) { + current = index + pos * stride; + + if (!git_oid_raw_ncmp(short_oid->id, current, len)) + found = 1; + } + } + + if (found && + len != p->oid_hexsize && + pos + 1 < (int)p->num_objects) { + /* Check for ambiguousity */ + const unsigned char *next = current + stride; + + if (!git_oid_raw_ncmp(short_oid->id, next, len)) { + found = 2; + } + } + + if (!found) { + error = git_odb__error_notfound("failed to find offset for pack entry", short_oid, len); + goto cleanup; + } + if (found > 1) { + error = git_odb__error_ambiguous("found multiple offsets for pack entry"); + goto cleanup; + } + + if ((offset = nth_packed_object_offset_locked(p, pos)) < 0) { + git_error_set(GIT_ERROR_ODB, "packfile index is corrupt"); + error = -1; + goto cleanup; + } + + *offset_out = offset; + git_oid__fromraw(found_oid, current, p->oid_type); + +#ifdef INDEX_DEBUG_LOOKUP + { + char hex_sha1[p->oid_hexsize + 1]; + git_oid_fmt(hex_sha1, found_oid); + hex_sha1[p->oid_hexsize] = '\0'; + printf("found lo=%d %s\n", lo, hex_sha1); + } +#endif + +cleanup: + git_mutex_unlock(&p->lock); + return error; +} + +int git_pack_entry_find( + struct git_pack_entry *e, + struct git_pack_file *p, + const git_oid *short_oid, + size_t len) +{ + off64_t offset; + git_oid found_oid; + int error; + + GIT_ASSERT_ARG(p); + + if (len == p->oid_hexsize && p->num_bad_objects) { + unsigned i; + for (i = 0; i < p->num_bad_objects; i++) + if (git_oid__cmp(short_oid, &p->bad_object_ids[i]) == 0) + return packfile_error("bad object found in packfile"); + } + + error = pack_entry_find_offset(&offset, &found_oid, p, short_oid, len); + if (error < 0) + return error; + + error = git_mutex_lock(&p->lock); + if (error < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock packfile reader"); + return error; + } + error = git_mutex_lock(&p->mwf.lock); + if (error < 0) { + git_mutex_unlock(&p->lock); + git_error_set(GIT_ERROR_OS, "failed to lock packfile reader"); + return error; + } + + /* we found a unique entry in the index; + * make sure the packfile backing the index + * still exists on disk */ + if (p->mwf.fd == -1) + error = packfile_open_locked(p); + git_mutex_unlock(&p->mwf.lock); + git_mutex_unlock(&p->lock); + if (error < 0) + return error; + + e->offset = offset; + e->p = p; + + git_oid_cpy(&e->id, &found_oid); + return 0; +} diff --git a/src/libgit2/pack.h b/src/libgit2/pack.h new file mode 100644 index 0000000..1a9eb14 --- /dev/null +++ b/src/libgit2/pack.h @@ -0,0 +1,214 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_pack_h__ +#define INCLUDE_pack_h__ + +#include "common.h" + +#include "git2/oid.h" + +#include "array.h" +#include "map.h" +#include "mwindow.h" +#include "odb.h" +#include "offmap.h" +#include "oidmap.h" +#include "zstream.h" +#include "oid.h" + +/** + * Function type for callbacks from git_pack_foreach_entry_offset. + */ +typedef int git_pack_foreach_entry_offset_cb( + const git_oid *id, + off64_t offset, + void *payload); + +#define GIT_PACK_FILE_MODE 0444 + +#define PACK_SIGNATURE 0x5041434b /* "PACK" */ +#define PACK_VERSION 2 +#define pack_version_ok(v) ((v) == htonl(2)) +struct git_pack_header { + uint32_t hdr_signature; + uint32_t hdr_version; + uint32_t hdr_entries; +}; + +/* + * The first four bytes of index formats later than version 1 should + * start with this signature, as all older git binaries would find this + * value illegal and abort reading the file. + * + * This is the case because the number of objects in a packfile + * cannot exceed 1,431,660,000 as every object would need at least + * 3 bytes of data and the overall packfile cannot exceed 4 GiB with + * version 1 of the index file due to the offsets limited to 32 bits. + * Clearly the signature exceeds this maximum. + * + * Very old git binaries will also compare the first 4 bytes to the + * next 4 bytes in the index and abort with a "non-monotonic index" + * error if the second 4 byte word is smaller than the first 4 + * byte word. This would be true in the proposed future index + * format as idx_signature would be greater than idx_version. + */ + +#define PACK_IDX_SIGNATURE 0xff744f63 /* "\377tOc" */ + +struct git_pack_idx_header { + uint32_t idx_signature; + uint32_t idx_version; +}; + +typedef struct git_pack_cache_entry { + size_t last_usage; /* enough? */ + git_atomic32 refcount; + git_rawobj raw; +} git_pack_cache_entry; + +struct pack_chain_elem { + off64_t base_key; + off64_t offset; + size_t size; + git_object_t type; +}; + +typedef git_array_t(struct pack_chain_elem) git_dependency_chain; + +#define GIT_PACK_CACHE_MEMORY_LIMIT 16 * 1024 * 1024 +#define GIT_PACK_CACHE_SIZE_LIMIT 1024 * 1024 /* don't bother caching anything over 1MB */ + +typedef struct { + size_t memory_used; + size_t memory_limit; + size_t use_ctr; + git_mutex lock; + git_offmap *entries; +} git_pack_cache; + +struct git_pack_file { + git_mwindow_file mwf; + git_map index_map; + git_mutex lock; /* protect updates to index_map */ + git_atomic32 refcount; + + uint32_t num_objects; + uint32_t num_bad_objects; + git_oid *bad_object_ids; /* array of git_oid */ + + git_oid_t oid_type; + unsigned oid_hexsize:7, + oid_size:6, + pack_local:1, + pack_keep:1, + has_cache:1; + + int index_version; + git_time_t mtime; + git_oidmap *idx_cache; + unsigned char **ids; + + git_pack_cache bases; /* delta base cache */ + + time_t last_freshen; /* last time the packfile was freshened */ + + /* something like ".git/objects/pack/xxxxx.pack" */ + char pack_name[GIT_FLEX_ARRAY]; /* more */ +}; + +/** + * Return the position where an OID (or a prefix) would be inserted within + * the OID Lookup Table of an .idx file. This performs binary search + * between the lo and hi indices. + * + * The stride parameter is provided because .idx files version 1 store the + * OIDs interleaved with the 4-byte file offsets of the objects within the + * .pack file (stride = oid_size + 4), whereas files with version 2 store + * them in a contiguous flat array (stride = oid_size). + */ +int git_pack__lookup_id( + const void *id_lookup_table, + size_t stride, + unsigned lo, + unsigned hi, + const unsigned char *id_prefix, + const git_oid_t oid_type); + +struct git_pack_entry { + off64_t offset; + git_oid id; + struct git_pack_file *p; +}; + +typedef struct git_packfile_stream { + off64_t curpos; + int done; + git_zstream zstream; + struct git_pack_file *p; + git_mwindow *mw; +} git_packfile_stream; + +int git_packfile__object_header(size_t *out, unsigned char *hdr, size_t size, git_object_t type); + +int git_packfile__name(char **out, const char *path); + +int git_packfile_unpack_header( + size_t *size_p, + git_object_t *type_p, + struct git_pack_file *p, + git_mwindow **w_curs, + off64_t *curpos); + +int git_packfile_resolve_header( + size_t *size_p, + git_object_t *type_p, + struct git_pack_file *p, + off64_t offset); + +int git_packfile_unpack(git_rawobj *obj, struct git_pack_file *p, off64_t *obj_offset); + +int git_packfile_stream_open(git_packfile_stream *obj, struct git_pack_file *p, off64_t curpos); +ssize_t git_packfile_stream_read(git_packfile_stream *obj, void *buffer, size_t len); +void git_packfile_stream_dispose(git_packfile_stream *obj); + +int get_delta_base( + off64_t *delta_base_out, + struct git_pack_file *p, + git_mwindow **w_curs, + off64_t *curpos, + git_object_t type, + off64_t delta_obj_offset); + +void git_packfile_free(struct git_pack_file *p, bool unlink_packfile); +int git_packfile_alloc( + struct git_pack_file **pack_out, + const char *path, + git_oid_t oid_type); + +int git_pack_entry_find( + struct git_pack_entry *e, + struct git_pack_file *p, + const git_oid *short_id, + size_t len); +int git_pack_foreach_entry( + struct git_pack_file *p, + git_odb_foreach_cb cb, + void *data); +/** + * Similar to git_pack_foreach_entry, but: + * - It also provides the offset of the object within the + * packfile. + * - It does not sort the objects in any order. + * - It retains the lock while invoking the callback. + */ +int git_pack_foreach_entry_offset( + struct git_pack_file *p, + git_pack_foreach_entry_offset_cb cb, + void *data); + +#endif diff --git a/src/libgit2/parse.c b/src/libgit2/parse.c new file mode 100644 index 0000000..9eb86a3 --- /dev/null +++ b/src/libgit2/parse.c @@ -0,0 +1,138 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#include "parse.h" +#include "oid.h" + +int git_parse_ctx_init(git_parse_ctx *ctx, const char *content, size_t content_len) +{ + if (content && content_len) { + ctx->content = content; + ctx->content_len = content_len; + } else { + ctx->content = ""; + ctx->content_len = 0; + } + + ctx->remain = ctx->content; + ctx->remain_len = ctx->content_len; + ctx->line = ctx->remain; + ctx->line_len = git__linenlen(ctx->line, ctx->remain_len); + ctx->line_num = 1; + + return 0; +} + +void git_parse_ctx_clear(git_parse_ctx *ctx) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->content = ""; +} + +void git_parse_advance_line(git_parse_ctx *ctx) +{ + ctx->line += ctx->line_len; + ctx->remain_len -= ctx->line_len; + ctx->line_len = git__linenlen(ctx->line, ctx->remain_len); + ctx->line_num++; +} + +void git_parse_advance_chars(git_parse_ctx *ctx, size_t char_cnt) +{ + ctx->line += char_cnt; + ctx->remain_len -= char_cnt; + ctx->line_len -= char_cnt; +} + +int git_parse_advance_expected( + git_parse_ctx *ctx, + const char *expected, + size_t expected_len) +{ + if (ctx->line_len < expected_len) + return -1; + + if (memcmp(ctx->line, expected, expected_len) != 0) + return -1; + + git_parse_advance_chars(ctx, expected_len); + return 0; +} + +int git_parse_advance_ws(git_parse_ctx *ctx) +{ + int ret = -1; + + while (ctx->line_len > 0 && + ctx->line[0] != '\n' && + git__isspace(ctx->line[0])) { + ctx->line++; + ctx->line_len--; + ctx->remain_len--; + ret = 0; + } + + return ret; +} + +int git_parse_advance_nl(git_parse_ctx *ctx) +{ + if (ctx->line_len != 1 || ctx->line[0] != '\n') + return -1; + + git_parse_advance_line(ctx); + return 0; +} + +int git_parse_advance_digit(int64_t *out, git_parse_ctx *ctx, int base) +{ + const char *end; + int ret; + + if (ctx->line_len < 1 || !git__isdigit(ctx->line[0])) + return -1; + + if ((ret = git__strntol64(out, ctx->line, ctx->line_len, &end, base)) < 0) + return -1; + + git_parse_advance_chars(ctx, (end - ctx->line)); + return 0; +} + +int git_parse_advance_oid(git_oid *out, git_parse_ctx *ctx, git_oid_t oid_type) +{ + size_t oid_hexsize = git_oid_hexsize(oid_type); + GIT_ASSERT(oid_hexsize); + + if (ctx->line_len < oid_hexsize) + return -1; + if ((git_oid__fromstrn(out, ctx->line, oid_hexsize, oid_type)) < 0) + return -1; + git_parse_advance_chars(ctx, oid_hexsize); + return 0; +} + +int git_parse_peek(char *out, git_parse_ctx *ctx, int flags) +{ + size_t remain = ctx->line_len; + const char *ptr = ctx->line; + + while (remain) { + char c = *ptr; + + if ((flags & GIT_PARSE_PEEK_SKIP_WHITESPACE) && + git__isspace(c)) { + remain--; + ptr++; + continue; + } + + *out = c; + return 0; + } + + return -1; +} diff --git a/src/libgit2/parse.h b/src/libgit2/parse.h new file mode 100644 index 0000000..beef1de --- /dev/null +++ b/src/libgit2/parse.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_parse_h__ +#define INCLUDE_parse_h__ + +#include "common.h" + +typedef struct { + /* Original content buffer */ + const char *content; + size_t content_len; + + /* The remaining (unparsed) buffer */ + const char *remain; + size_t remain_len; + + const char *line; + size_t line_len; + size_t line_num; +} git_parse_ctx; + +#define GIT_PARSE_CTX_INIT { 0 } + +int git_parse_ctx_init(git_parse_ctx *ctx, const char *content, size_t content_len); +void git_parse_ctx_clear(git_parse_ctx *ctx); + +#define git_parse_ctx_contains_s(ctx, str) \ + git_parse_ctx_contains(ctx, str, sizeof(str) - 1) + +GIT_INLINE(bool) git_parse_ctx_contains( + git_parse_ctx *ctx, const char *str, size_t len) +{ + return (ctx->line_len >= len && memcmp(ctx->line, str, len) == 0); +} + +void git_parse_advance_line(git_parse_ctx *ctx); +void git_parse_advance_chars(git_parse_ctx *ctx, size_t char_cnt); +int git_parse_advance_expected( + git_parse_ctx *ctx, + const char *expected, + size_t expected_len); + +#define git_parse_advance_expected_str(ctx, str) \ + git_parse_advance_expected(ctx, str, strlen(str)) + +int git_parse_advance_ws(git_parse_ctx *ctx); +int git_parse_advance_nl(git_parse_ctx *ctx); +int git_parse_advance_digit(int64_t *out, git_parse_ctx *ctx, int base); +int git_parse_advance_oid(git_oid *out, git_parse_ctx *ctx, git_oid_t oid_type); + +enum GIT_PARSE_PEEK_FLAGS { + GIT_PARSE_PEEK_SKIP_WHITESPACE = (1 << 0) +}; + +int git_parse_peek(char *out, git_parse_ctx *ctx, int flags); + +#endif diff --git a/src/libgit2/patch.c b/src/libgit2/patch.c new file mode 100644 index 0000000..a30546f --- /dev/null +++ b/src/libgit2/patch.c @@ -0,0 +1,230 @@ +/* +* Copyright (C) the libgit2 contributors. All rights reserved. +* +* This file is part of libgit2, distributed under the GNU GPL v2 with +* a Linking Exception. For full terms see the included COPYING file. +*/ + +#include "patch.h" + +#include "git2/patch.h" +#include "diff.h" + +int git_patch__invoke_callbacks( + git_patch *patch, + git_diff_file_cb file_cb, + git_diff_binary_cb binary_cb, + git_diff_hunk_cb hunk_cb, + git_diff_line_cb line_cb, + void *payload) +{ + int error = 0; + uint32_t i, j; + + if (file_cb) + error = file_cb(patch->delta, 0, payload); + + if (error) + return error; + + if ((patch->delta->flags & GIT_DIFF_FLAG_BINARY) != 0) { + if (binary_cb) + error = binary_cb(patch->delta, &patch->binary, payload); + + return error; + } + + if (!hunk_cb && !line_cb) + return error; + + for (i = 0; !error && i < git_array_size(patch->hunks); ++i) { + git_patch_hunk *h = git_array_get(patch->hunks, i); + + if (hunk_cb) + error = hunk_cb(patch->delta, &h->hunk, payload); + + if (!line_cb) + continue; + + for (j = 0; !error && j < h->line_count; ++j) { + git_diff_line *l = + git_array_get(patch->lines, h->line_start + j); + + error = line_cb(patch->delta, &h->hunk, l, payload); + } + } + + return error; +} + +size_t git_patch_size( + git_patch *patch, + int include_context, + int include_hunk_headers, + int include_file_headers) +{ + size_t out; + + GIT_ASSERT_ARG(patch); + + out = patch->content_size; + + if (!include_context) + out -= patch->context_size; + + if (include_hunk_headers) + out += patch->header_size; + + if (include_file_headers) { + git_str file_header = GIT_STR_INIT; + + if (git_diff_delta__format_file_header( + &file_header, patch->delta, NULL, NULL, 0, true) < 0) + git_error_clear(); + else + out += git_str_len(&file_header); + + git_str_dispose(&file_header); + } + + return out; +} + +int git_patch_line_stats( + size_t *total_ctxt, + size_t *total_adds, + size_t *total_dels, + const git_patch *patch) +{ + size_t totals[3], idx; + + memset(totals, 0, sizeof(totals)); + + for (idx = 0; idx < git_array_size(patch->lines); ++idx) { + git_diff_line *line = git_array_get(patch->lines, idx); + if (!line) + continue; + + switch (line->origin) { + case GIT_DIFF_LINE_CONTEXT: totals[0]++; break; + case GIT_DIFF_LINE_ADDITION: totals[1]++; break; + case GIT_DIFF_LINE_DELETION: totals[2]++; break; + default: + /* diff --stat and --numstat don't count EOFNL marks because + * they will always be paired with a ADDITION or DELETION line. + */ + break; + } + } + + if (total_ctxt) + *total_ctxt = totals[0]; + if (total_adds) + *total_adds = totals[1]; + if (total_dels) + *total_dels = totals[2]; + + return 0; +} + +const git_diff_delta *git_patch_get_delta(const git_patch *patch) +{ + GIT_ASSERT_ARG_WITH_RETVAL(patch, NULL); + return patch->delta; +} + +size_t git_patch_num_hunks(const git_patch *patch) +{ + GIT_ASSERT_ARG(patch); + return git_array_size(patch->hunks); +} + +static int patch_error_outofrange(const char *thing) +{ + git_error_set(GIT_ERROR_INVALID, "patch %s index out of range", thing); + return GIT_ENOTFOUND; +} + +int git_patch_get_hunk( + const git_diff_hunk **out, + size_t *lines_in_hunk, + git_patch *patch, + size_t hunk_idx) +{ + git_patch_hunk *hunk; + GIT_ASSERT_ARG(patch); + + hunk = git_array_get(patch->hunks, hunk_idx); + + if (!hunk) { + if (out) *out = NULL; + if (lines_in_hunk) *lines_in_hunk = 0; + return patch_error_outofrange("hunk"); + } + + if (out) *out = &hunk->hunk; + if (lines_in_hunk) *lines_in_hunk = hunk->line_count; + return 0; +} + +int git_patch_num_lines_in_hunk(const git_patch *patch, size_t hunk_idx) +{ + git_patch_hunk *hunk; + GIT_ASSERT_ARG(patch); + + if (!(hunk = git_array_get(patch->hunks, hunk_idx))) + return patch_error_outofrange("hunk"); + return (int)hunk->line_count; +} + +int git_patch_get_line_in_hunk( + const git_diff_line **out, + git_patch *patch, + size_t hunk_idx, + size_t line_of_hunk) +{ + git_patch_hunk *hunk; + git_diff_line *line; + + GIT_ASSERT_ARG(patch); + + if (!(hunk = git_array_get(patch->hunks, hunk_idx))) { + if (out) *out = NULL; + return patch_error_outofrange("hunk"); + } + + if (line_of_hunk >= hunk->line_count || + !(line = git_array_get( + patch->lines, hunk->line_start + line_of_hunk))) { + if (out) *out = NULL; + return patch_error_outofrange("line"); + } + + if (out) *out = line; + return 0; +} + +git_repository *git_patch_owner(const git_patch *patch) +{ + return patch->repo; +} + +int git_patch_from_diff(git_patch **out, git_diff *diff, size_t idx) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(diff); + GIT_ASSERT_ARG(diff->patch_fn); + return diff->patch_fn(out, diff, idx); +} + +static void git_patch__free(git_patch *patch) +{ + if (patch->free_fn) + patch->free_fn(patch); +} + +void git_patch_free(git_patch *patch) +{ + if (patch) + GIT_REFCOUNT_DEC(patch, git_patch__free); +} diff --git a/src/libgit2/patch.h b/src/libgit2/patch.h new file mode 100644 index 0000000..86328e8 --- /dev/null +++ b/src/libgit2/patch.h @@ -0,0 +1,75 @@ +/* +* Copyright (C) the libgit2 contributors. All rights reserved. +* +* This file is part of libgit2, distributed under the GNU GPL v2 with +* a Linking Exception. For full terms see the included COPYING file. +*/ +#ifndef INCLUDE_patch_h__ +#define INCLUDE_patch_h__ + +#include "common.h" + +#include "git2/patch.h" +#include "array.h" + +/* cached information about a hunk in a patch */ +typedef struct git_patch_hunk { + git_diff_hunk hunk; + size_t line_start; + size_t line_count; +} git_patch_hunk; + +struct git_patch { + git_refcount rc; + + git_repository *repo; /* may be null */ + + git_diff_options diff_opts; + + git_diff_delta *delta; + git_diff_binary binary; + git_array_t(git_patch_hunk) hunks; + git_array_t(git_diff_line) lines; + + size_t header_size; + size_t content_size; + size_t context_size; + + void (*free_fn)(git_patch *patch); +}; + +extern int git_patch__invoke_callbacks( + git_patch *patch, + git_diff_file_cb file_cb, + git_diff_binary_cb binary_cb, + git_diff_hunk_cb hunk_cb, + git_diff_line_cb line_cb, + void *payload); + +extern int git_patch_line_stats( + size_t *total_ctxt, + size_t *total_adds, + size_t *total_dels, + const git_patch *patch); + +/** Options for parsing patch files. */ +typedef struct { + /** + * The length of the prefix (in path segments) for the filenames. + * This prefix will be removed when looking for files. The default is 1. + */ + uint32_t prefix_len; + + /** + * The type of object IDs in the patch file. The default is + * `GIT_OID_DEFAULT`. + */ + git_oid_t oid_type; +} git_patch_options; + +#define GIT_PATCH_OPTIONS_INIT { 1, GIT_OID_DEFAULT } + +extern int git_patch__to_buf(git_str *out, git_patch *patch); +extern void git_patch_free(git_patch *patch); + +#endif diff --git a/src/libgit2/patch_generate.c b/src/libgit2/patch_generate.c new file mode 100644 index 0000000..079bc53 --- /dev/null +++ b/src/libgit2/patch_generate.c @@ -0,0 +1,934 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "patch_generate.h" + +#include "git2/blob.h" +#include "diff.h" +#include "diff_generate.h" +#include "diff_file.h" +#include "diff_driver.h" +#include "diff_xdiff.h" +#include "delta.h" +#include "zstream.h" +#include "futils.h" + +static void diff_output_init( + git_patch_generated_output *, const git_diff_options *, git_diff_file_cb, + git_diff_binary_cb, git_diff_hunk_cb, git_diff_line_cb, void*); + +static void diff_output_to_patch( + git_patch_generated_output *, git_patch_generated *); + +static void patch_generated_free(git_patch *p) +{ + git_patch_generated *patch = (git_patch_generated *)p; + + git_array_clear(patch->base.lines); + git_array_clear(patch->base.hunks); + + git__free((char *)patch->base.binary.old_file.data); + git__free((char *)patch->base.binary.new_file.data); + + git_diff_file_content__clear(&patch->ofile); + git_diff_file_content__clear(&patch->nfile); + + git_diff_free(patch->diff); /* decrements refcount */ + patch->diff = NULL; + + git_pool_clear(&patch->flattened); + + git__free((char *)patch->base.diff_opts.old_prefix); + git__free((char *)patch->base.diff_opts.new_prefix); + + if (patch->flags & GIT_PATCH_GENERATED_ALLOCATED) + git__free(patch); +} + +static void patch_generated_update_binary(git_patch_generated *patch) +{ + if ((patch->base.delta->flags & DIFF_FLAGS_KNOWN_BINARY) != 0) + return; + + if ((patch->ofile.file->flags & GIT_DIFF_FLAG_BINARY) != 0 || + (patch->nfile.file->flags & GIT_DIFF_FLAG_BINARY) != 0) + patch->base.delta->flags |= GIT_DIFF_FLAG_BINARY; + + else if (patch->ofile.file->size > GIT_XDIFF_MAX_SIZE || + patch->nfile.file->size > GIT_XDIFF_MAX_SIZE) + patch->base.delta->flags |= GIT_DIFF_FLAG_BINARY; + + else if ((patch->ofile.file->flags & DIFF_FLAGS_NOT_BINARY) != 0 && + (patch->nfile.file->flags & DIFF_FLAGS_NOT_BINARY) != 0) + patch->base.delta->flags |= GIT_DIFF_FLAG_NOT_BINARY; +} + +static void patch_generated_init_common(git_patch_generated *patch) +{ + patch->base.free_fn = patch_generated_free; + + patch_generated_update_binary(patch); + + patch->flags |= GIT_PATCH_GENERATED_INITIALIZED; + + if (patch->diff) + git_diff_addref(patch->diff); +} + +static int patch_generated_normalize_options( + git_diff_options *out, + const git_diff_options *opts, + git_repository *repo) +{ + if (opts) { + GIT_ERROR_CHECK_VERSION(opts, GIT_DIFF_OPTIONS_VERSION, "git_diff_options"); + memcpy(out, opts, sizeof(git_diff_options)); + } else { + git_diff_options default_opts = GIT_DIFF_OPTIONS_INIT; + memcpy(out, &default_opts, sizeof(git_diff_options)); + } + + if (repo && opts && opts->oid_type && repo->oid_type != opts->oid_type) { + /* + * This limitation feels unnecessary - we should consider + * allowing users to generate diffs with a different object + * ID format than the repository. + */ + git_error_set(GIT_ERROR_INVALID, + "specified object ID type does not match repository object ID type"); + return -1; + } else if (repo) { + out->oid_type = repo->oid_type; + } else if (opts && opts->oid_type) { + out->oid_type = opts->oid_type; + } else { + out->oid_type = GIT_OID_DEFAULT; + } + + out->old_prefix = opts && opts->old_prefix ? + git__strdup(opts->old_prefix) : + git__strdup(DIFF_OLD_PREFIX_DEFAULT); + + out->new_prefix = opts && opts->new_prefix ? + git__strdup(opts->new_prefix) : + git__strdup(DIFF_NEW_PREFIX_DEFAULT); + + GIT_ERROR_CHECK_ALLOC(out->old_prefix); + GIT_ERROR_CHECK_ALLOC(out->new_prefix); + + return 0; +} + +static int patch_generated_init( + git_patch_generated *patch, git_diff *diff, size_t delta_index) +{ + int error = 0; + + memset(patch, 0, sizeof(*patch)); + + patch->diff = diff; + patch->base.repo = diff->repo; + patch->base.delta = git_vector_get(&diff->deltas, delta_index); + patch->delta_index = delta_index; + + if ((error = patch_generated_normalize_options( + &patch->base.diff_opts, &diff->opts, diff->repo)) < 0 || + (error = git_diff_file_content__init_from_diff( + &patch->ofile, diff, patch->base.delta, true)) < 0 || + (error = git_diff_file_content__init_from_diff( + &patch->nfile, diff, patch->base.delta, false)) < 0) + return error; + + patch_generated_init_common(patch); + + return 0; +} + +static int patch_generated_alloc_from_diff( + git_patch_generated **out, git_diff *diff, size_t delta_index) +{ + int error; + git_patch_generated *patch = git__calloc(1, sizeof(git_patch_generated)); + GIT_ERROR_CHECK_ALLOC(patch); + + if (!(error = patch_generated_init(patch, diff, delta_index))) { + patch->flags |= GIT_PATCH_GENERATED_ALLOCATED; + GIT_REFCOUNT_INC(&patch->base); + } else { + git__free(patch); + patch = NULL; + } + + *out = patch; + return error; +} + +GIT_INLINE(bool) should_skip_binary(git_patch_generated *patch, git_diff_file *file) +{ + if ((patch->base.diff_opts.flags & GIT_DIFF_SHOW_BINARY) != 0) + return false; + + return (file->flags & GIT_DIFF_FLAG_BINARY) != 0; +} + +static bool patch_generated_diffable(git_patch_generated *patch) +{ + size_t olen, nlen; + + if (patch->base.delta->status == GIT_DELTA_UNMODIFIED) + return false; + + /* if we've determined this to be binary (and we are not showing binary + * data) then we have skipped loading the map data. instead, query the + * file data itself. + */ + if ((patch->base.delta->flags & GIT_DIFF_FLAG_BINARY) != 0 && + (patch->base.diff_opts.flags & GIT_DIFF_SHOW_BINARY) == 0) { + olen = (size_t)patch->ofile.file->size; + nlen = (size_t)patch->nfile.file->size; + } else { + olen = patch->ofile.map.len; + nlen = patch->nfile.map.len; + } + + /* if both sides are empty, files are identical */ + if (!olen && !nlen) + return false; + + /* otherwise, check the file sizes and the oid */ + return (olen != nlen || + !git_oid_equal(&patch->ofile.file->id, &patch->nfile.file->id)); +} + +static int patch_generated_load(git_patch_generated *patch, git_patch_generated_output *output) +{ + int error = 0; + bool incomplete_data; + + if ((patch->flags & GIT_PATCH_GENERATED_LOADED) != 0) + return 0; + + /* if no hunk and data callbacks and user doesn't care if data looks + * binary, then there is no need to actually load the data + */ + if ((patch->ofile.opts_flags & GIT_DIFF_SKIP_BINARY_CHECK) != 0 && + output && !output->binary_cb && !output->hunk_cb && !output->data_cb) + return 0; + + incomplete_data = + (((patch->ofile.flags & GIT_DIFF_FLAG__NO_DATA) != 0 || + (patch->ofile.file->flags & GIT_DIFF_FLAG_VALID_ID) != 0) && + ((patch->nfile.flags & GIT_DIFF_FLAG__NO_DATA) != 0 || + (patch->nfile.file->flags & GIT_DIFF_FLAG_VALID_ID) != 0)); + + if ((error = git_diff_file_content__load( + &patch->ofile, &patch->base.diff_opts)) < 0 || + (error = git_diff_file_content__load( + &patch->nfile, &patch->base.diff_opts)) < 0 || + should_skip_binary(patch, patch->nfile.file)) + goto cleanup; + + /* if previously missing an oid, and now that we have it the two sides + * are the same (and not submodules), update MODIFIED -> UNMODIFIED + */ + if (incomplete_data && + patch->ofile.file->mode == patch->nfile.file->mode && + patch->ofile.file->mode != GIT_FILEMODE_COMMIT && + git_oid_equal(&patch->ofile.file->id, &patch->nfile.file->id) && + patch->base.delta->status == GIT_DELTA_MODIFIED) /* not RENAMED/COPIED! */ + patch->base.delta->status = GIT_DELTA_UNMODIFIED; + +cleanup: + patch_generated_update_binary(patch); + + if (!error) { + if (patch_generated_diffable(patch)) + patch->flags |= GIT_PATCH_GENERATED_DIFFABLE; + + patch->flags |= GIT_PATCH_GENERATED_LOADED; + } + + return error; +} + +static int patch_generated_invoke_file_callback( + git_patch_generated *patch, git_patch_generated_output *output) +{ + float progress = patch->diff ? + ((float)patch->delta_index / patch->diff->deltas.length) : 1.0f; + + if (!output->file_cb) + return 0; + + return git_error_set_after_callback_function( + output->file_cb(patch->base.delta, progress, output->payload), + "git_patch"); +} + +static int create_binary( + git_diff_binary_t *out_type, + char **out_data, + size_t *out_datalen, + size_t *out_inflatedlen, + const char *a_data, + size_t a_datalen, + const char *b_data, + size_t b_datalen) +{ + git_str deflate = GIT_STR_INIT, delta = GIT_STR_INIT; + size_t delta_data_len = 0; + int error; + + /* The git_delta function accepts unsigned long only */ + if (!git__is_ulong(a_datalen) || !git__is_ulong(b_datalen)) + return GIT_EBUFS; + + if ((error = git_zstream_deflatebuf(&deflate, b_data, b_datalen)) < 0) + goto done; + + /* The git_delta function accepts unsigned long only */ + if (!git__is_ulong(deflate.size)) { + error = GIT_EBUFS; + goto done; + } + + if (a_datalen && b_datalen) { + void *delta_data; + + error = git_delta(&delta_data, &delta_data_len, + a_data, a_datalen, + b_data, b_datalen, + deflate.size); + + if (error == 0) { + error = git_zstream_deflatebuf( + &delta, delta_data, delta_data_len); + + git__free(delta_data); + } else if (error == GIT_EBUFS) { + error = 0; + } + + if (error < 0) + goto done; + } + + if (delta.size && delta.size < deflate.size) { + *out_type = GIT_DIFF_BINARY_DELTA; + *out_datalen = delta.size; + *out_data = git_str_detach(&delta); + *out_inflatedlen = delta_data_len; + } else { + *out_type = GIT_DIFF_BINARY_LITERAL; + *out_datalen = deflate.size; + *out_data = git_str_detach(&deflate); + *out_inflatedlen = b_datalen; + } + +done: + git_str_dispose(&deflate); + git_str_dispose(&delta); + + return error; +} + +static int diff_binary(git_patch_generated_output *output, git_patch_generated *patch) +{ + git_diff_binary binary = {0}; + const char *old_data = patch->ofile.map.data; + const char *new_data = patch->nfile.map.data; + size_t old_len = patch->ofile.map.len, + new_len = patch->nfile.map.len; + int error; + + /* Only load contents if the user actually wants to diff + * binary files. */ + if (patch->base.diff_opts.flags & GIT_DIFF_SHOW_BINARY) { + binary.contains_data = 1; + + /* Create the old->new delta (as the "new" side of the patch), + * and the new->old delta (as the "old" side) + */ + if ((error = create_binary(&binary.old_file.type, + (char **)&binary.old_file.data, + &binary.old_file.datalen, + &binary.old_file.inflatedlen, + new_data, new_len, old_data, old_len)) < 0 || + (error = create_binary(&binary.new_file.type, + (char **)&binary.new_file.data, + &binary.new_file.datalen, + &binary.new_file.inflatedlen, + old_data, old_len, new_data, new_len)) < 0) + return error; + } + + error = git_error_set_after_callback_function( + output->binary_cb(patch->base.delta, &binary, output->payload), + "git_patch"); + + git__free((char *) binary.old_file.data); + git__free((char *) binary.new_file.data); + + return error; +} + +static int patch_generated_create( + git_patch_generated *patch, + git_patch_generated_output *output) +{ + int error = 0; + + if ((patch->flags & GIT_PATCH_GENERATED_DIFFED) != 0) + return 0; + + /* if we are not looking at the binary or text data, don't do the diff */ + if (!output->binary_cb && !output->hunk_cb && !output->data_cb) + return 0; + + if ((patch->flags & GIT_PATCH_GENERATED_LOADED) == 0 && + (error = patch_generated_load(patch, output)) < 0) + return error; + + if ((patch->flags & GIT_PATCH_GENERATED_DIFFABLE) == 0) + return 0; + + if ((patch->base.delta->flags & GIT_DIFF_FLAG_BINARY) != 0) { + if (output->binary_cb) + error = diff_binary(output, patch); + } + else { + if (output->diff_cb) + error = output->diff_cb(output, patch); + } + + patch->flags |= GIT_PATCH_GENERATED_DIFFED; + return error; +} + +static int diff_required(git_diff *diff, const char *action) +{ + if (diff) + return 0; + git_error_set(GIT_ERROR_INVALID, "must provide valid diff to %s", action); + return -1; +} + +typedef struct { + git_patch_generated patch; + git_diff_delta delta; + char paths[GIT_FLEX_ARRAY]; +} patch_generated_with_delta; + +static int diff_single_generate(patch_generated_with_delta *pd, git_xdiff_output *xo) +{ + int error = 0; + git_patch_generated *patch = &pd->patch; + bool has_old = ((patch->ofile.flags & GIT_DIFF_FLAG__NO_DATA) == 0); + bool has_new = ((patch->nfile.flags & GIT_DIFF_FLAG__NO_DATA) == 0); + + pd->delta.status = has_new ? + (has_old ? GIT_DELTA_MODIFIED : GIT_DELTA_ADDED) : + (has_old ? GIT_DELTA_DELETED : GIT_DELTA_UNTRACKED); + + if (git_oid_equal(&patch->nfile.file->id, &patch->ofile.file->id)) + pd->delta.status = GIT_DELTA_UNMODIFIED; + + patch->base.delta = &pd->delta; + + patch_generated_init_common(patch); + + if (pd->delta.status == GIT_DELTA_UNMODIFIED && + !(patch->ofile.opts_flags & GIT_DIFF_INCLUDE_UNMODIFIED)) { + + /* Even empty patches are flagged as binary, and even though + * there's no difference, we flag this as "containing data" + * (the data is known to be empty, as opposed to wholly unknown). + */ + if (patch->base.diff_opts.flags & GIT_DIFF_SHOW_BINARY) + patch->base.binary.contains_data = 1; + + return error; + } + + error = patch_generated_invoke_file_callback(patch, (git_patch_generated_output *)xo); + + if (!error) + error = patch_generated_create(patch, (git_patch_generated_output *)xo); + + return error; +} + +static int patch_generated_from_sources( + patch_generated_with_delta *pd, + git_xdiff_output *xo, + git_diff_file_content_src *oldsrc, + git_diff_file_content_src *newsrc, + const git_diff_options *given_opts) +{ + int error = 0; + git_repository *repo = + oldsrc->blob ? git_blob_owner(oldsrc->blob) : + newsrc->blob ? git_blob_owner(newsrc->blob) : NULL; + git_diff_file *lfile = &pd->delta.old_file, *rfile = &pd->delta.new_file; + git_diff_file_content *ldata = &pd->patch.ofile, *rdata = &pd->patch.nfile; + git_diff_options *opts = &pd->patch.base.diff_opts; + + if ((error = patch_generated_normalize_options(opts, given_opts, repo)) < 0) + return error; + + if ((opts->flags & GIT_DIFF_REVERSE) != 0) { + void *tmp = lfile; lfile = rfile; rfile = tmp; + tmp = ldata; ldata = rdata; rdata = tmp; + } + + pd->patch.base.delta = &pd->delta; + + if (!oldsrc->as_path) { + if (newsrc->as_path) + oldsrc->as_path = newsrc->as_path; + else + oldsrc->as_path = newsrc->as_path = "file"; + } + else if (!newsrc->as_path) + newsrc->as_path = oldsrc->as_path; + + lfile->path = oldsrc->as_path; + rfile->path = newsrc->as_path; + + if ((error = git_diff_file_content__init_from_src( + ldata, repo, opts, oldsrc, lfile)) < 0 || + (error = git_diff_file_content__init_from_src( + rdata, repo, opts, newsrc, rfile)) < 0) + return error; + + return diff_single_generate(pd, xo); +} + +static int patch_generated_with_delta_alloc( + patch_generated_with_delta **out, + const char **old_path, + const char **new_path) +{ + patch_generated_with_delta *pd; + size_t old_len = *old_path ? strlen(*old_path) : 0; + size_t new_len = *new_path ? strlen(*new_path) : 0; + size_t alloc_len; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, sizeof(*pd), old_len); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, new_len); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, 2); + + *out = pd = git__calloc(1, alloc_len); + GIT_ERROR_CHECK_ALLOC(pd); + + pd->patch.flags = GIT_PATCH_GENERATED_ALLOCATED; + + if (*old_path) { + memcpy(&pd->paths[0], *old_path, old_len); + *old_path = &pd->paths[0]; + } else if (*new_path) + *old_path = &pd->paths[old_len + 1]; + + if (*new_path) { + memcpy(&pd->paths[old_len + 1], *new_path, new_len); + *new_path = &pd->paths[old_len + 1]; + } else if (*old_path) + *new_path = &pd->paths[0]; + + return 0; +} + +static int diff_from_sources( + git_diff_file_content_src *oldsrc, + git_diff_file_content_src *newsrc, + const git_diff_options *opts, + git_diff_file_cb file_cb, + git_diff_binary_cb binary_cb, + git_diff_hunk_cb hunk_cb, + git_diff_line_cb data_cb, + void *payload) +{ + int error = 0; + patch_generated_with_delta pd; + git_xdiff_output xo; + + memset(&xo, 0, sizeof(xo)); + diff_output_init( + &xo.output, opts, file_cb, binary_cb, hunk_cb, data_cb, payload); + git_xdiff_init(&xo, opts); + + memset(&pd, 0, sizeof(pd)); + + error = patch_generated_from_sources(&pd, &xo, oldsrc, newsrc, opts); + + git_patch_free(&pd.patch.base); + + return error; +} + +static int patch_from_sources( + git_patch **out, + git_diff_file_content_src *oldsrc, + git_diff_file_content_src *newsrc, + const git_diff_options *opts) +{ + int error = 0; + patch_generated_with_delta *pd; + git_xdiff_output xo; + + GIT_ASSERT_ARG(out); + *out = NULL; + + if ((error = patch_generated_with_delta_alloc( + &pd, &oldsrc->as_path, &newsrc->as_path)) < 0) + return error; + + memset(&xo, 0, sizeof(xo)); + diff_output_to_patch(&xo.output, &pd->patch); + git_xdiff_init(&xo, opts); + + if (!(error = patch_generated_from_sources(pd, &xo, oldsrc, newsrc, opts))) + *out = (git_patch *)pd; + else + git_patch_free((git_patch *)pd); + + return error; +} + +int git_diff_blobs( + const git_blob *old_blob, + const char *old_path, + const git_blob *new_blob, + const char *new_path, + const git_diff_options *opts, + git_diff_file_cb file_cb, + git_diff_binary_cb binary_cb, + git_diff_hunk_cb hunk_cb, + git_diff_line_cb data_cb, + void *payload) +{ + git_diff_file_content_src osrc = + GIT_DIFF_FILE_CONTENT_SRC__BLOB(old_blob, old_path); + git_diff_file_content_src nsrc = + GIT_DIFF_FILE_CONTENT_SRC__BLOB(new_blob, new_path); + return diff_from_sources( + &osrc, &nsrc, opts, file_cb, binary_cb, hunk_cb, data_cb, payload); +} + +int git_patch_from_blobs( + git_patch **out, + const git_blob *old_blob, + const char *old_path, + const git_blob *new_blob, + const char *new_path, + const git_diff_options *opts) +{ + git_diff_file_content_src osrc = + GIT_DIFF_FILE_CONTENT_SRC__BLOB(old_blob, old_path); + git_diff_file_content_src nsrc = + GIT_DIFF_FILE_CONTENT_SRC__BLOB(new_blob, new_path); + return patch_from_sources(out, &osrc, &nsrc, opts); +} + +int git_diff_blob_to_buffer( + const git_blob *old_blob, + const char *old_path, + const char *buf, + size_t buflen, + const char *buf_path, + const git_diff_options *opts, + git_diff_file_cb file_cb, + git_diff_binary_cb binary_cb, + git_diff_hunk_cb hunk_cb, + git_diff_line_cb data_cb, + void *payload) +{ + git_diff_file_content_src osrc = + GIT_DIFF_FILE_CONTENT_SRC__BLOB(old_blob, old_path); + git_diff_file_content_src nsrc = + GIT_DIFF_FILE_CONTENT_SRC__BUF(buf, buflen, buf_path); + return diff_from_sources( + &osrc, &nsrc, opts, file_cb, binary_cb, hunk_cb, data_cb, payload); +} + +int git_patch_from_blob_and_buffer( + git_patch **out, + const git_blob *old_blob, + const char *old_path, + const void *buf, + size_t buflen, + const char *buf_path, + const git_diff_options *opts) +{ + git_diff_file_content_src osrc = + GIT_DIFF_FILE_CONTENT_SRC__BLOB(old_blob, old_path); + git_diff_file_content_src nsrc = + GIT_DIFF_FILE_CONTENT_SRC__BUF(buf, buflen, buf_path); + return patch_from_sources(out, &osrc, &nsrc, opts); +} + +int git_diff_buffers( + const void *old_buf, + size_t old_len, + const char *old_path, + const void *new_buf, + size_t new_len, + const char *new_path, + const git_diff_options *opts, + git_diff_file_cb file_cb, + git_diff_binary_cb binary_cb, + git_diff_hunk_cb hunk_cb, + git_diff_line_cb data_cb, + void *payload) +{ + git_diff_file_content_src osrc = + GIT_DIFF_FILE_CONTENT_SRC__BUF(old_buf, old_len, old_path); + git_diff_file_content_src nsrc = + GIT_DIFF_FILE_CONTENT_SRC__BUF(new_buf, new_len, new_path); + return diff_from_sources( + &osrc, &nsrc, opts, file_cb, binary_cb, hunk_cb, data_cb, payload); +} + +int git_patch_from_buffers( + git_patch **out, + const void *old_buf, + size_t old_len, + const char *old_path, + const void *new_buf, + size_t new_len, + const char *new_path, + const git_diff_options *opts) +{ + git_diff_file_content_src osrc = + GIT_DIFF_FILE_CONTENT_SRC__BUF(old_buf, old_len, old_path); + git_diff_file_content_src nsrc = + GIT_DIFF_FILE_CONTENT_SRC__BUF(new_buf, new_len, new_path); + return patch_from_sources(out, &osrc, &nsrc, opts); +} + +int git_patch_generated_from_diff( + git_patch **patch_ptr, git_diff *diff, size_t idx) +{ + int error = 0; + git_xdiff_output xo; + git_diff_delta *delta = NULL; + git_patch_generated *patch = NULL; + + if (patch_ptr) *patch_ptr = NULL; + + if (diff_required(diff, "git_patch_from_diff") < 0) + return -1; + + delta = git_vector_get(&diff->deltas, idx); + if (!delta) { + git_error_set(GIT_ERROR_INVALID, "index out of range for delta in diff"); + return GIT_ENOTFOUND; + } + + if (git_diff_delta__should_skip(&diff->opts, delta)) + return 0; + + /* don't load the patch data unless we need it for binary check */ + if (!patch_ptr && + ((delta->flags & DIFF_FLAGS_KNOWN_BINARY) != 0 || + (diff->opts.flags & GIT_DIFF_SKIP_BINARY_CHECK) != 0)) + return 0; + + if ((error = patch_generated_alloc_from_diff(&patch, diff, idx)) < 0) + return error; + + memset(&xo, 0, sizeof(xo)); + diff_output_to_patch(&xo.output, patch); + git_xdiff_init(&xo, &diff->opts); + + error = patch_generated_invoke_file_callback(patch, &xo.output); + + if (!error) + error = patch_generated_create(patch, &xo.output); + + if (!error) { + /* TODO: if cumulative diff size is < 0.5 total size, flatten patch */ + /* TODO: and unload the file content */ + } + + if (error || !patch_ptr) + git_patch_free(&patch->base); + else + *patch_ptr = &patch->base; + + return error; +} + +git_diff_driver *git_patch_generated_driver(git_patch_generated *patch) +{ + /* ofile driver is representative for whole patch */ + return patch->ofile.driver; +} + +int git_patch_generated_old_data( + char **ptr, long *len, git_patch_generated *patch) +{ + if (patch->ofile.map.len > LONG_MAX || + patch->ofile.map.len > GIT_XDIFF_MAX_SIZE) { + git_error_set(GIT_ERROR_INVALID, "files too large for diff"); + return -1; + } + + *ptr = patch->ofile.map.data; + *len = (long)patch->ofile.map.len; + + return 0; +} + +int git_patch_generated_new_data( + char **ptr, long *len, git_patch_generated *patch) +{ + if (patch->ofile.map.len > LONG_MAX || + patch->ofile.map.len > GIT_XDIFF_MAX_SIZE) { + git_error_set(GIT_ERROR_INVALID, "files too large for diff"); + return -1; + } + + *ptr = patch->nfile.map.data; + *len = (long)patch->nfile.map.len; + + return 0; +} + +static int patch_generated_file_cb( + const git_diff_delta *delta, + float progress, + void *payload) +{ + GIT_UNUSED(delta); GIT_UNUSED(progress); GIT_UNUSED(payload); + return 0; +} + +static int patch_generated_binary_cb( + const git_diff_delta *delta, + const git_diff_binary *binary, + void *payload) +{ + git_patch *patch = payload; + + GIT_UNUSED(delta); + + memcpy(&patch->binary, binary, sizeof(git_diff_binary)); + + if (binary->old_file.data) { + patch->binary.old_file.data = git__malloc(binary->old_file.datalen); + GIT_ERROR_CHECK_ALLOC(patch->binary.old_file.data); + + memcpy((char *)patch->binary.old_file.data, + binary->old_file.data, binary->old_file.datalen); + } + + if (binary->new_file.data) { + patch->binary.new_file.data = git__malloc(binary->new_file.datalen); + GIT_ERROR_CHECK_ALLOC(patch->binary.new_file.data); + + memcpy((char *)patch->binary.new_file.data, + binary->new_file.data, binary->new_file.datalen); + } + + return 0; +} + +static int git_patch_hunk_cb( + const git_diff_delta *delta, + const git_diff_hunk *hunk_, + void *payload) +{ + git_patch_generated *patch = payload; + git_patch_hunk *hunk; + + GIT_UNUSED(delta); + + hunk = git_array_alloc(patch->base.hunks); + GIT_ERROR_CHECK_ALLOC(hunk); + + memcpy(&hunk->hunk, hunk_, sizeof(hunk->hunk)); + + patch->base.header_size += hunk_->header_len; + + hunk->line_start = git_array_size(patch->base.lines); + hunk->line_count = 0; + + return 0; +} + +static int patch_generated_line_cb( + const git_diff_delta *delta, + const git_diff_hunk *hunk_, + const git_diff_line *line_, + void *payload) +{ + git_patch_generated *patch = payload; + git_patch_hunk *hunk; + git_diff_line *line; + + GIT_UNUSED(delta); + GIT_UNUSED(hunk_); + + hunk = git_array_last(patch->base.hunks); + GIT_ASSERT(hunk); /* programmer error if no hunk is available */ + + line = git_array_alloc(patch->base.lines); + GIT_ERROR_CHECK_ALLOC(line); + + memcpy(line, line_, sizeof(*line)); + + /* do some bookkeeping so we can provide old/new line numbers */ + + patch->base.content_size += line->content_len; + + if (line->origin == GIT_DIFF_LINE_ADDITION || + line->origin == GIT_DIFF_LINE_DELETION) + patch->base.content_size += 1; + else if (line->origin == GIT_DIFF_LINE_CONTEXT) { + patch->base.content_size += 1; + patch->base.context_size += line->content_len + 1; + } else if (line->origin == GIT_DIFF_LINE_CONTEXT_EOFNL) + patch->base.context_size += line->content_len; + + hunk->line_count++; + + return 0; +} + +static void diff_output_init( + git_patch_generated_output *out, + const git_diff_options *opts, + git_diff_file_cb file_cb, + git_diff_binary_cb binary_cb, + git_diff_hunk_cb hunk_cb, + git_diff_line_cb data_cb, + void *payload) +{ + GIT_UNUSED(opts); + + memset(out, 0, sizeof(*out)); + + out->file_cb = file_cb; + out->binary_cb = binary_cb; + out->hunk_cb = hunk_cb; + out->data_cb = data_cb; + out->payload = payload; +} + +static void diff_output_to_patch( + git_patch_generated_output *out, git_patch_generated *patch) +{ + diff_output_init( + out, + NULL, + patch_generated_file_cb, + patch_generated_binary_cb, + git_patch_hunk_cb, + patch_generated_line_cb, + patch); +} diff --git a/src/libgit2/patch_generate.h b/src/libgit2/patch_generate.h new file mode 100644 index 0000000..56e3e9d --- /dev/null +++ b/src/libgit2/patch_generate.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_patch_generate_h__ +#define INCLUDE_patch_generate_h__ + +#include "common.h" + +#include "diff.h" +#include "diff_file.h" +#include "patch.h" + +enum { + GIT_PATCH_GENERATED_ALLOCATED = (1 << 0), + GIT_PATCH_GENERATED_INITIALIZED = (1 << 1), + GIT_PATCH_GENERATED_LOADED = (1 << 2), + /* the two sides are different */ + GIT_PATCH_GENERATED_DIFFABLE = (1 << 3), + /* the difference between the two sides has been computed */ + GIT_PATCH_GENERATED_DIFFED = (1 << 4), + GIT_PATCH_GENERATED_FLATTENED = (1 << 5) +}; + +struct git_patch_generated { + struct git_patch base; + + git_diff *diff; /* for refcount purposes, maybe NULL for blob diffs */ + size_t delta_index; + git_diff_file_content ofile; + git_diff_file_content nfile; + uint32_t flags; + git_pool flattened; +}; + +typedef struct git_patch_generated git_patch_generated; + +extern git_diff_driver *git_patch_generated_driver(git_patch_generated *); + +extern int git_patch_generated_old_data( + char **, long *, git_patch_generated *); +extern int git_patch_generated_new_data( + char **, long *, git_patch_generated *); +extern int git_patch_generated_from_diff( + git_patch **, git_diff *, size_t); + +typedef struct git_patch_generated_output git_patch_generated_output; + +struct git_patch_generated_output { + /* these callbacks are issued with the diff data */ + git_diff_file_cb file_cb; + git_diff_binary_cb binary_cb; + git_diff_hunk_cb hunk_cb; + git_diff_line_cb data_cb; + void *payload; + + /* this records the actual error in cases where it may be obscured */ + int error; + + /* this callback is used to do the diff and drive the other callbacks. + * see diff_xdiff.h for how to use this in practice for now. + */ + int (*diff_cb)(git_patch_generated_output *output, + git_patch_generated *patch); +}; + +#endif diff --git a/src/libgit2/patch_parse.c b/src/libgit2/patch_parse.c new file mode 100644 index 0000000..c069155 --- /dev/null +++ b/src/libgit2/patch_parse.c @@ -0,0 +1,1239 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "patch_parse.h" + +#include "git2/patch.h" +#include "patch.h" +#include "diff_parse.h" +#include "fs_path.h" + +typedef struct { + git_patch base; + + git_patch_parse_ctx *ctx; + + /* the paths from the `diff --git` header, these will be used if this is not + * a rename (and rename paths are specified) or if no `+++`/`---` line specify + * the paths. + */ + char *header_old_path, *header_new_path; + + /* renamed paths are precise and are not prefixed */ + char *rename_old_path, *rename_new_path; + + /* the paths given in `---` and `+++` lines */ + char *old_path, *new_path; + + /* the prefixes from the old/new paths */ + char *old_prefix, *new_prefix; +} git_patch_parsed; + +static int git_parse_err(const char *fmt, ...) GIT_FORMAT_PRINTF(1, 2); +static int git_parse_err(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + git_error_vset(GIT_ERROR_PATCH, fmt, ap); + va_end(ap); + + return -1; +} + +static size_t header_path_len(git_patch_parse_ctx *ctx) +{ + bool inquote = 0; + bool quoted = git_parse_ctx_contains_s(&ctx->parse_ctx, "\""); + size_t len; + + for (len = quoted; len < ctx->parse_ctx.line_len; len++) { + if (!quoted && git__isspace(ctx->parse_ctx.line[len])) + break; + else if (quoted && !inquote && ctx->parse_ctx.line[len] == '"') { + len++; + break; + } + + inquote = (!inquote && ctx->parse_ctx.line[len] == '\\'); + } + + return len; +} + +static int parse_header_path_buf(git_str *path, git_patch_parse_ctx *ctx, size_t path_len) +{ + int error; + + if ((error = git_str_put(path, ctx->parse_ctx.line, path_len)) < 0) + return error; + + git_parse_advance_chars(&ctx->parse_ctx, path_len); + + git_str_rtrim(path); + + if (path->size > 0 && path->ptr[0] == '"' && + (error = git_str_unquote(path)) < 0) + return error; + + git_fs_path_squash_slashes(path); + + if (!path->size) + return git_parse_err("patch contains empty path at line %"PRIuZ, + ctx->parse_ctx.line_num); + + return 0; +} + +static int parse_header_path(char **out, git_patch_parse_ctx *ctx) +{ + git_str path = GIT_STR_INIT; + int error; + + if ((error = parse_header_path_buf(&path, ctx, header_path_len(ctx))) < 0) + goto out; + *out = git_str_detach(&path); + +out: + git_str_dispose(&path); + return error; +} + +static int parse_header_git_oldpath( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + git_str old_path = GIT_STR_INIT; + int error; + + if (patch->old_path) { + error = git_parse_err("patch contains duplicate old path at line %"PRIuZ, + ctx->parse_ctx.line_num); + goto out; + } + + if ((error = parse_header_path_buf(&old_path, ctx, ctx->parse_ctx.line_len - 1)) < 0) + goto out; + + patch->old_path = git_str_detach(&old_path); + +out: + git_str_dispose(&old_path); + return error; +} + +static int parse_header_git_newpath( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + git_str new_path = GIT_STR_INIT; + int error; + + if (patch->new_path) { + error = git_parse_err("patch contains duplicate new path at line %"PRIuZ, + ctx->parse_ctx.line_num); + goto out; + } + + if ((error = parse_header_path_buf(&new_path, ctx, ctx->parse_ctx.line_len - 1)) < 0) + goto out; + patch->new_path = git_str_detach(&new_path); + +out: + git_str_dispose(&new_path); + return error; +} + +static int parse_header_mode(uint16_t *mode, git_patch_parse_ctx *ctx) +{ + int64_t m; + + if ((git_parse_advance_digit(&m, &ctx->parse_ctx, 8)) < 0) + return git_parse_err("invalid file mode at line %"PRIuZ, ctx->parse_ctx.line_num); + + if (m > UINT16_MAX) + return -1; + + *mode = (uint16_t)m; + + return 0; +} + +static int parse_header_oid( + git_oid *oid, + uint16_t *oid_len, + git_patch_parse_ctx *ctx) +{ + size_t hexsize, len; + + hexsize = git_oid_hexsize(ctx->opts.oid_type); + + for (len = 0; + len < ctx->parse_ctx.line_len && len < hexsize; + len++) { + if (!git__isxdigit(ctx->parse_ctx.line[len])) + break; + } + + if (len < GIT_OID_MINPREFIXLEN || len > hexsize || + git_oid__fromstrn(oid, ctx->parse_ctx.line, len, ctx->opts.oid_type) < 0) + return git_parse_err("invalid hex formatted object id at line %"PRIuZ, + ctx->parse_ctx.line_num); + + git_parse_advance_chars(&ctx->parse_ctx, len); + + *oid_len = (uint16_t)len; + + return 0; +} + +static int parse_header_git_index( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + char c; + + if (parse_header_oid(&patch->base.delta->old_file.id, + &patch->base.delta->old_file.id_abbrev, ctx) < 0 || + git_parse_advance_expected_str(&ctx->parse_ctx, "..") < 0 || + parse_header_oid(&patch->base.delta->new_file.id, + &patch->base.delta->new_file.id_abbrev, ctx) < 0) + return -1; + + if (git_parse_peek(&c, &ctx->parse_ctx, 0) == 0 && c == ' ') { + uint16_t mode = 0; + + git_parse_advance_chars(&ctx->parse_ctx, 1); + + if (parse_header_mode(&mode, ctx) < 0) + return -1; + + if (!patch->base.delta->new_file.mode) + patch->base.delta->new_file.mode = mode; + + if (!patch->base.delta->old_file.mode) + patch->base.delta->old_file.mode = mode; + } + + return 0; +} + +static int parse_header_git_oldmode( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + return parse_header_mode(&patch->base.delta->old_file.mode, ctx); +} + +static int parse_header_git_newmode( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + return parse_header_mode(&patch->base.delta->new_file.mode, ctx); +} + +static int parse_header_git_deletedfilemode( + git_patch_parsed *patch, + git_patch_parse_ctx *ctx) +{ + git__free((char *)patch->base.delta->new_file.path); + + patch->base.delta->new_file.path = NULL; + patch->base.delta->status = GIT_DELTA_DELETED; + patch->base.delta->nfiles = 1; + + return parse_header_mode(&patch->base.delta->old_file.mode, ctx); +} + +static int parse_header_git_newfilemode( + git_patch_parsed *patch, + git_patch_parse_ctx *ctx) +{ + git__free((char *)patch->base.delta->old_file.path); + + patch->base.delta->old_file.path = NULL; + patch->base.delta->status = GIT_DELTA_ADDED; + patch->base.delta->nfiles = 1; + + return parse_header_mode(&patch->base.delta->new_file.mode, ctx); +} + +static int parse_header_rename( + char **out, + git_patch_parse_ctx *ctx) +{ + git_str path = GIT_STR_INIT; + + if (parse_header_path_buf(&path, ctx, header_path_len(ctx)) < 0) + return -1; + + /* Note: the `rename from` and `rename to` lines include the literal + * filename. They do *not* include the prefix. (Who needs consistency?) + */ + *out = git_str_detach(&path); + return 0; +} + +static int parse_header_renamefrom( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + patch->base.delta->status = GIT_DELTA_RENAMED; + return parse_header_rename(&patch->rename_old_path, ctx); +} + +static int parse_header_renameto( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + patch->base.delta->status = GIT_DELTA_RENAMED; + return parse_header_rename(&patch->rename_new_path, ctx); +} + +static int parse_header_copyfrom( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + patch->base.delta->status = GIT_DELTA_COPIED; + return parse_header_rename(&patch->rename_old_path, ctx); +} + +static int parse_header_copyto( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + patch->base.delta->status = GIT_DELTA_COPIED; + return parse_header_rename(&patch->rename_new_path, ctx); +} + +static int parse_header_percent(uint16_t *out, git_patch_parse_ctx *ctx) +{ + int64_t val; + + if (git_parse_advance_digit(&val, &ctx->parse_ctx, 10) < 0) + return -1; + + if (git_parse_advance_expected_str(&ctx->parse_ctx, "%") < 0) + return -1; + + if (val < 0 || val > 100) + return -1; + + *out = (uint16_t)val; + return 0; +} + +static int parse_header_similarity( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + if (parse_header_percent(&patch->base.delta->similarity, ctx) < 0) + return git_parse_err("invalid similarity percentage at line %"PRIuZ, + ctx->parse_ctx.line_num); + + return 0; +} + +static int parse_header_dissimilarity( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + uint16_t dissimilarity; + + if (parse_header_percent(&dissimilarity, ctx) < 0) + return git_parse_err("invalid similarity percentage at line %"PRIuZ, + ctx->parse_ctx.line_num); + + patch->base.delta->similarity = 100 - dissimilarity; + + return 0; +} + +static int parse_header_start(git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + if (parse_header_path(&patch->header_old_path, ctx) < 0) + return git_parse_err("corrupt old path in git diff header at line %"PRIuZ, + ctx->parse_ctx.line_num); + + if (git_parse_advance_ws(&ctx->parse_ctx) < 0 || + parse_header_path(&patch->header_new_path, ctx) < 0) + return git_parse_err("corrupt new path in git diff header at line %"PRIuZ, + ctx->parse_ctx.line_num); + + /* + * We cannot expect to be able to always parse paths correctly at this + * point. Due to the possibility of unquoted names, whitespaces in + * filenames and custom prefixes we have to allow that, though, and just + * proceed here. We then hope for the "---" and "+++" lines to fix that + * for us. + */ + if (!git_parse_ctx_contains(&ctx->parse_ctx, "\n", 1) && + !git_parse_ctx_contains(&ctx->parse_ctx, "\r\n", 2)) { + git_parse_advance_chars(&ctx->parse_ctx, ctx->parse_ctx.line_len - 1); + + git__free(patch->header_old_path); + patch->header_old_path = NULL; + git__free(patch->header_new_path); + patch->header_new_path = NULL; + } + + return 0; +} + +typedef enum { + STATE_START, + + STATE_DIFF, + STATE_FILEMODE, + STATE_MODE, + STATE_INDEX, + STATE_PATH, + + STATE_SIMILARITY, + STATE_RENAME, + STATE_COPY, + + STATE_END +} parse_header_state; + +typedef struct { + const char *str; + parse_header_state expected_state; + parse_header_state next_state; + int(*fn)(git_patch_parsed *, git_patch_parse_ctx *); +} parse_header_transition; + +static const parse_header_transition transitions[] = { + /* Start */ + { "diff --git " , STATE_START, STATE_DIFF, parse_header_start }, + + { "deleted file mode " , STATE_DIFF, STATE_FILEMODE, parse_header_git_deletedfilemode }, + { "new file mode " , STATE_DIFF, STATE_FILEMODE, parse_header_git_newfilemode }, + { "old mode " , STATE_DIFF, STATE_MODE, parse_header_git_oldmode }, + { "new mode " , STATE_MODE, STATE_END, parse_header_git_newmode }, + + { "index " , STATE_FILEMODE, STATE_INDEX, parse_header_git_index }, + { "index " , STATE_DIFF, STATE_INDEX, parse_header_git_index }, + { "index " , STATE_END, STATE_INDEX, parse_header_git_index }, + + { "--- " , STATE_DIFF, STATE_PATH, parse_header_git_oldpath }, + { "--- " , STATE_INDEX, STATE_PATH, parse_header_git_oldpath }, + { "--- " , STATE_FILEMODE, STATE_PATH, parse_header_git_oldpath }, + { "+++ " , STATE_PATH, STATE_END, parse_header_git_newpath }, + { "GIT binary patch" , STATE_INDEX, STATE_END, NULL }, + { "Binary files " , STATE_INDEX, STATE_END, NULL }, + + { "similarity index " , STATE_END, STATE_SIMILARITY, parse_header_similarity }, + { "similarity index " , STATE_DIFF, STATE_SIMILARITY, parse_header_similarity }, + { "dissimilarity index ", STATE_DIFF, STATE_SIMILARITY, parse_header_dissimilarity }, + { "rename from " , STATE_SIMILARITY, STATE_RENAME, parse_header_renamefrom }, + { "rename old " , STATE_SIMILARITY, STATE_RENAME, parse_header_renamefrom }, + { "copy from " , STATE_SIMILARITY, STATE_COPY, parse_header_copyfrom }, + { "rename to " , STATE_RENAME, STATE_END, parse_header_renameto }, + { "rename new " , STATE_RENAME, STATE_END, parse_header_renameto }, + { "copy to " , STATE_COPY, STATE_END, parse_header_copyto }, + + /* Next patch */ + { "diff --git " , STATE_END, 0, NULL }, + { "@@ -" , STATE_END, 0, NULL }, + { "-- " , STATE_INDEX, 0, NULL }, + { "-- " , STATE_END, 0, NULL }, +}; + +static int parse_header_git( + git_patch_parsed *patch, + git_patch_parse_ctx *ctx) +{ + size_t i; + int error = 0; + parse_header_state state = STATE_START; + + /* Parse remaining header lines */ + for (; ctx->parse_ctx.remain_len > 0; git_parse_advance_line(&ctx->parse_ctx)) { + bool found = false; + + if (ctx->parse_ctx.line_len == 0 || ctx->parse_ctx.line[ctx->parse_ctx.line_len - 1] != '\n') + break; + + for (i = 0; i < ARRAY_SIZE(transitions); i++) { + const parse_header_transition *transition = &transitions[i]; + size_t op_len = strlen(transition->str); + + if (transition->expected_state != state || + git__prefixcmp(ctx->parse_ctx.line, transition->str) != 0) + continue; + + state = transition->next_state; + + /* Do not advance if this is the patch separator */ + if (transition->fn == NULL) + goto done; + + git_parse_advance_chars(&ctx->parse_ctx, op_len); + + if ((error = transition->fn(patch, ctx)) < 0) + goto done; + + git_parse_advance_ws(&ctx->parse_ctx); + + if (git_parse_advance_expected_str(&ctx->parse_ctx, "\n") < 0 || + ctx->parse_ctx.line_len > 0) { + error = git_parse_err("trailing data at line %"PRIuZ, ctx->parse_ctx.line_num); + goto done; + } + + found = true; + break; + } + + if (!found) { + error = git_parse_err("invalid patch header at line %"PRIuZ, + ctx->parse_ctx.line_num); + goto done; + } + } + + if (state != STATE_END) { + error = git_parse_err("unexpected header line %"PRIuZ, ctx->parse_ctx.line_num); + goto done; + } + +done: + return error; +} + +static int parse_int(int *out, git_patch_parse_ctx *ctx) +{ + int64_t num; + + if (git_parse_advance_digit(&num, &ctx->parse_ctx, 10) < 0 || !git__is_int(num)) + return -1; + + *out = (int)num; + return 0; +} + +static int parse_hunk_header( + git_patch_hunk *hunk, + git_patch_parse_ctx *ctx) +{ + const char *header_start = ctx->parse_ctx.line; + char c; + + hunk->hunk.old_lines = 1; + hunk->hunk.new_lines = 1; + + if (git_parse_advance_expected_str(&ctx->parse_ctx, "@@ -") < 0 || + parse_int(&hunk->hunk.old_start, ctx) < 0) + goto fail; + + if (git_parse_peek(&c, &ctx->parse_ctx, 0) == 0 && c == ',') { + if (git_parse_advance_expected_str(&ctx->parse_ctx, ",") < 0 || + parse_int(&hunk->hunk.old_lines, ctx) < 0) + goto fail; + } + + if (git_parse_advance_expected_str(&ctx->parse_ctx, " +") < 0 || + parse_int(&hunk->hunk.new_start, ctx) < 0) + goto fail; + + if (git_parse_peek(&c, &ctx->parse_ctx, 0) == 0 && c == ',') { + if (git_parse_advance_expected_str(&ctx->parse_ctx, ",") < 0 || + parse_int(&hunk->hunk.new_lines, ctx) < 0) + goto fail; + } + + if (git_parse_advance_expected_str(&ctx->parse_ctx, " @@") < 0) + goto fail; + + git_parse_advance_line(&ctx->parse_ctx); + + if (!hunk->hunk.old_lines && !hunk->hunk.new_lines) + goto fail; + + hunk->hunk.header_len = ctx->parse_ctx.line - header_start; + if (hunk->hunk.header_len > (GIT_DIFF_HUNK_HEADER_SIZE - 1)) + return git_parse_err("oversized patch hunk header at line %"PRIuZ, + ctx->parse_ctx.line_num); + + memcpy(hunk->hunk.header, header_start, hunk->hunk.header_len); + hunk->hunk.header[hunk->hunk.header_len] = '\0'; + + return 0; + +fail: + git_error_set(GIT_ERROR_PATCH, "invalid patch hunk header at line %"PRIuZ, + ctx->parse_ctx.line_num); + return -1; +} + +static int eof_for_origin(int origin) { + if (origin == GIT_DIFF_LINE_ADDITION) + return GIT_DIFF_LINE_ADD_EOFNL; + if (origin == GIT_DIFF_LINE_DELETION) + return GIT_DIFF_LINE_DEL_EOFNL; + return GIT_DIFF_LINE_CONTEXT_EOFNL; +} + +static int parse_hunk_body( + git_patch_parsed *patch, + git_patch_hunk *hunk, + git_patch_parse_ctx *ctx) +{ + git_diff_line *line; + int error = 0; + + int oldlines = hunk->hunk.old_lines; + int newlines = hunk->hunk.new_lines; + int last_origin = 0; + + for (; + ctx->parse_ctx.remain_len > 1 && + (oldlines || newlines) && + !git_parse_ctx_contains_s(&ctx->parse_ctx, "@@ -"); + git_parse_advance_line(&ctx->parse_ctx)) { + + int old_lineno, new_lineno, origin, prefix = 1; + char c; + + if (git__add_int_overflow(&old_lineno, hunk->hunk.old_start, hunk->hunk.old_lines) || + git__sub_int_overflow(&old_lineno, old_lineno, oldlines) || + git__add_int_overflow(&new_lineno, hunk->hunk.new_start, hunk->hunk.new_lines) || + git__sub_int_overflow(&new_lineno, new_lineno, newlines)) { + error = git_parse_err("unrepresentable line count at line %"PRIuZ, + ctx->parse_ctx.line_num); + goto done; + } + + if (ctx->parse_ctx.line_len == 0 || ctx->parse_ctx.line[ctx->parse_ctx.line_len - 1] != '\n') { + error = git_parse_err("invalid patch instruction at line %"PRIuZ, + ctx->parse_ctx.line_num); + goto done; + } + + git_parse_peek(&c, &ctx->parse_ctx, 0); + + switch (c) { + case '\n': + prefix = 0; + /* fall through */ + + case ' ': + origin = GIT_DIFF_LINE_CONTEXT; + oldlines--; + newlines--; + break; + + case '-': + origin = GIT_DIFF_LINE_DELETION; + oldlines--; + new_lineno = -1; + break; + + case '+': + origin = GIT_DIFF_LINE_ADDITION; + newlines--; + old_lineno = -1; + break; + + case '\\': + /* + * If there are no oldlines left, then this is probably + * the "\ No newline at end of file" marker. Do not + * verify its format, as it may be localized. + */ + if (!oldlines) { + prefix = 0; + origin = eof_for_origin(last_origin); + old_lineno = -1; + new_lineno = -1; + break; + } + /* fall through */ + + default: + error = git_parse_err("invalid patch hunk at line %"PRIuZ, ctx->parse_ctx.line_num); + goto done; + } + + line = git_array_alloc(patch->base.lines); + GIT_ERROR_CHECK_ALLOC(line); + + memset(line, 0x0, sizeof(git_diff_line)); + + line->content_len = ctx->parse_ctx.line_len - prefix; + line->content = git__strndup(ctx->parse_ctx.line + prefix, line->content_len); + GIT_ERROR_CHECK_ALLOC(line->content); + line->content_offset = ctx->parse_ctx.content_len - ctx->parse_ctx.remain_len; + line->origin = origin; + line->num_lines = 1; + line->old_lineno = old_lineno; + line->new_lineno = new_lineno; + + hunk->line_count++; + + last_origin = origin; + } + + if (oldlines || newlines) { + error = git_parse_err( + "invalid patch hunk, expected %d old lines and %d new lines", + hunk->hunk.old_lines, hunk->hunk.new_lines); + goto done; + } + + /* + * Handle "\ No newline at end of file". Only expect the leading + * backslash, though, because the rest of the string could be + * localized. Because `diff` optimizes for the case where you + * want to apply the patch by hand. + */ + if (git_parse_ctx_contains_s(&ctx->parse_ctx, "\\ ") && + git_array_size(patch->base.lines) > 0) { + + line = git_array_get(patch->base.lines, git_array_size(patch->base.lines) - 1); + + if (line->content_len < 1) { + error = git_parse_err("last line has no trailing newline"); + goto done; + } + + line = git_array_alloc(patch->base.lines); + GIT_ERROR_CHECK_ALLOC(line); + + memset(line, 0x0, sizeof(git_diff_line)); + + line->content_len = ctx->parse_ctx.line_len; + line->content = git__strndup(ctx->parse_ctx.line, line->content_len); + GIT_ERROR_CHECK_ALLOC(line->content); + line->content_offset = ctx->parse_ctx.content_len - ctx->parse_ctx.remain_len; + line->origin = eof_for_origin(last_origin); + line->num_lines = 1; + line->old_lineno = -1; + line->new_lineno = -1; + + hunk->line_count++; + + git_parse_advance_line(&ctx->parse_ctx); + } + +done: + return error; +} + +static int parse_patch_header( + git_patch_parsed *patch, + git_patch_parse_ctx *ctx) +{ + int error = 0; + + for (; ctx->parse_ctx.remain_len > 0; git_parse_advance_line(&ctx->parse_ctx)) { + /* This line is too short to be a patch header. */ + if (ctx->parse_ctx.line_len < 6) + continue; + + /* This might be a hunk header without a patch header, provide a + * sensible error message. */ + if (git_parse_ctx_contains_s(&ctx->parse_ctx, "@@ -")) { + size_t line_num = ctx->parse_ctx.line_num; + git_patch_hunk hunk; + + /* If this cannot be parsed as a hunk header, it's just leading + * noise, continue. + */ + if (parse_hunk_header(&hunk, ctx) < 0) { + git_error_clear(); + continue; + } + + error = git_parse_err("invalid hunk header outside patch at line %"PRIuZ, + line_num); + goto done; + } + + /* This buffer is too short to contain a patch. */ + if (ctx->parse_ctx.remain_len < ctx->parse_ctx.line_len + 6) + break; + + /* A proper git patch */ + if (git_parse_ctx_contains_s(&ctx->parse_ctx, "diff --git ")) { + error = parse_header_git(patch, ctx); + goto done; + } + + error = 0; + continue; + } + + git_error_set(GIT_ERROR_PATCH, "no patch found"); + error = GIT_ENOTFOUND; + +done: + return error; +} + +static int parse_patch_binary_side( + git_diff_binary_file *binary, + git_patch_parse_ctx *ctx) +{ + git_diff_binary_t type = GIT_DIFF_BINARY_NONE; + git_str base85 = GIT_STR_INIT, decoded = GIT_STR_INIT; + int64_t len; + int error = 0; + + if (git_parse_ctx_contains_s(&ctx->parse_ctx, "literal ")) { + type = GIT_DIFF_BINARY_LITERAL; + git_parse_advance_chars(&ctx->parse_ctx, 8); + } else if (git_parse_ctx_contains_s(&ctx->parse_ctx, "delta ")) { + type = GIT_DIFF_BINARY_DELTA; + git_parse_advance_chars(&ctx->parse_ctx, 6); + } else { + error = git_parse_err( + "unknown binary delta type at line %"PRIuZ, ctx->parse_ctx.line_num); + goto done; + } + + if (git_parse_advance_digit(&len, &ctx->parse_ctx, 10) < 0 || + git_parse_advance_nl(&ctx->parse_ctx) < 0 || len < 0) { + error = git_parse_err("invalid binary size at line %"PRIuZ, ctx->parse_ctx.line_num); + goto done; + } + + while (ctx->parse_ctx.line_len) { + char c; + size_t encoded_len, decoded_len = 0, decoded_orig = decoded.size; + + git_parse_peek(&c, &ctx->parse_ctx, 0); + + if (c == '\n') + break; + else if (c >= 'A' && c <= 'Z') + decoded_len = c - 'A' + 1; + else if (c >= 'a' && c <= 'z') + decoded_len = c - 'a' + (('z' - 'a') + 1) + 1; + + if (!decoded_len) { + error = git_parse_err("invalid binary length at line %"PRIuZ, ctx->parse_ctx.line_num); + goto done; + } + + git_parse_advance_chars(&ctx->parse_ctx, 1); + + encoded_len = ((decoded_len / 4) + !!(decoded_len % 4)) * 5; + + if (!encoded_len || !ctx->parse_ctx.line_len || encoded_len > ctx->parse_ctx.line_len - 1) { + error = git_parse_err("truncated binary data at line %"PRIuZ, ctx->parse_ctx.line_num); + goto done; + } + + if ((error = git_str_decode_base85( + &decoded, ctx->parse_ctx.line, encoded_len, decoded_len)) < 0) + goto done; + + if (decoded.size - decoded_orig != decoded_len) { + error = git_parse_err("truncated binary data at line %"PRIuZ, ctx->parse_ctx.line_num); + goto done; + } + + git_parse_advance_chars(&ctx->parse_ctx, encoded_len); + + if (git_parse_advance_nl(&ctx->parse_ctx) < 0) { + error = git_parse_err("trailing data at line %"PRIuZ, ctx->parse_ctx.line_num); + goto done; + } + } + + binary->type = type; + binary->inflatedlen = (size_t)len; + binary->datalen = decoded.size; + binary->data = git_str_detach(&decoded); + +done: + git_str_dispose(&base85); + git_str_dispose(&decoded); + return error; +} + +static int parse_patch_binary( + git_patch_parsed *patch, + git_patch_parse_ctx *ctx) +{ + int error; + + if (git_parse_advance_expected_str(&ctx->parse_ctx, "GIT binary patch") < 0 || + git_parse_advance_nl(&ctx->parse_ctx) < 0) + return git_parse_err("corrupt git binary header at line %"PRIuZ, ctx->parse_ctx.line_num); + + /* parse old->new binary diff */ + if ((error = parse_patch_binary_side( + &patch->base.binary.new_file, ctx)) < 0) + return error; + + if (git_parse_advance_nl(&ctx->parse_ctx) < 0) + return git_parse_err("corrupt git binary separator at line %"PRIuZ, + ctx->parse_ctx.line_num); + + /* parse new->old binary diff */ + if ((error = parse_patch_binary_side( + &patch->base.binary.old_file, ctx)) < 0) + return error; + + if (git_parse_advance_nl(&ctx->parse_ctx) < 0) + return git_parse_err("corrupt git binary patch separator at line %"PRIuZ, + ctx->parse_ctx.line_num); + + patch->base.binary.contains_data = 1; + patch->base.delta->flags |= GIT_DIFF_FLAG_BINARY; + return 0; +} + +static int parse_patch_binary_nodata( + git_patch_parsed *patch, + git_patch_parse_ctx *ctx) +{ + const char *old = patch->old_path ? patch->old_path : patch->header_old_path; + const char *new = patch->new_path ? patch->new_path : patch->header_new_path; + + if (!old || !new) + return git_parse_err("corrupt binary data without paths at line %"PRIuZ, ctx->parse_ctx.line_num); + + if (patch->base.delta->status == GIT_DELTA_ADDED) + old = "/dev/null"; + else if (patch->base.delta->status == GIT_DELTA_DELETED) + new = "/dev/null"; + + if (git_parse_advance_expected_str(&ctx->parse_ctx, "Binary files ") < 0 || + git_parse_advance_expected_str(&ctx->parse_ctx, old) < 0 || + git_parse_advance_expected_str(&ctx->parse_ctx, " and ") < 0 || + git_parse_advance_expected_str(&ctx->parse_ctx, new) < 0 || + git_parse_advance_expected_str(&ctx->parse_ctx, " differ") < 0 || + git_parse_advance_nl(&ctx->parse_ctx) < 0) + return git_parse_err("corrupt git binary header at line %"PRIuZ, ctx->parse_ctx.line_num); + + patch->base.binary.contains_data = 0; + patch->base.delta->flags |= GIT_DIFF_FLAG_BINARY; + return 0; +} + +static int parse_patch_hunks( + git_patch_parsed *patch, + git_patch_parse_ctx *ctx) +{ + git_patch_hunk *hunk; + int error = 0; + + while (git_parse_ctx_contains_s(&ctx->parse_ctx, "@@ -")) { + hunk = git_array_alloc(patch->base.hunks); + GIT_ERROR_CHECK_ALLOC(hunk); + + memset(hunk, 0, sizeof(git_patch_hunk)); + + hunk->line_start = git_array_size(patch->base.lines); + hunk->line_count = 0; + + if ((error = parse_hunk_header(hunk, ctx)) < 0 || + (error = parse_hunk_body(patch, hunk, ctx)) < 0) + goto done; + } + + patch->base.delta->flags |= GIT_DIFF_FLAG_NOT_BINARY; + +done: + return error; +} + +static int parse_patch_body( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + if (git_parse_ctx_contains_s(&ctx->parse_ctx, "GIT binary patch")) + return parse_patch_binary(patch, ctx); + else if (git_parse_ctx_contains_s(&ctx->parse_ctx, "Binary files ")) + return parse_patch_binary_nodata(patch, ctx); + else + return parse_patch_hunks(patch, ctx); +} + +static int check_header_names( + const char *one, + const char *two, + const char *old_or_new, + bool two_null) +{ + if (!one || !two) + return 0; + + if (two_null && strcmp(two, "/dev/null") != 0) + return git_parse_err("expected %s path of '/dev/null'", old_or_new); + + else if (!two_null && strcmp(one, two) != 0) + return git_parse_err("mismatched %s path names", old_or_new); + + return 0; +} + +static int check_prefix( + char **out, + size_t *out_len, + git_patch_parsed *patch, + const char *path_start) +{ + const char *path = path_start; + size_t prefix_len = patch->ctx->opts.prefix_len; + size_t remain_len = prefix_len; + + *out = NULL; + *out_len = 0; + + if (prefix_len == 0) + goto done; + + /* leading slashes do not count as part of the prefix in git apply */ + while (*path == '/') + path++; + + while (*path && remain_len) { + if (*path == '/') + remain_len--; + + path++; + } + + if (remain_len || !*path) + return git_parse_err( + "header filename does not contain %"PRIuZ" path components", + prefix_len); + +done: + *out_len = (path - path_start); + *out = git__strndup(path_start, *out_len); + + return (*out == NULL) ? -1 : 0; +} + +static int check_filenames(git_patch_parsed *patch) +{ + const char *prefixed_new, *prefixed_old; + size_t old_prefixlen = 0, new_prefixlen = 0; + bool added = (patch->base.delta->status == GIT_DELTA_ADDED); + bool deleted = (patch->base.delta->status == GIT_DELTA_DELETED); + + if (patch->old_path && !patch->new_path) + return git_parse_err("missing new path"); + + if (!patch->old_path && patch->new_path) + return git_parse_err("missing old path"); + + /* Ensure (non-renamed) paths match */ + if (check_header_names(patch->header_old_path, patch->old_path, "old", added) < 0 || + check_header_names(patch->header_new_path, patch->new_path, "new", deleted) < 0) + return -1; + + prefixed_old = (!added && patch->old_path) ? patch->old_path : patch->header_old_path; + prefixed_new = (!deleted && patch->new_path) ? patch->new_path : patch->header_new_path; + + if ((prefixed_old && check_prefix(&patch->old_prefix, &old_prefixlen, patch, prefixed_old) < 0) || + (prefixed_new && check_prefix(&patch->new_prefix, &new_prefixlen, patch, prefixed_new) < 0)) + return -1; + + /* Prefer the rename filenames as they are unambiguous and unprefixed */ + if (patch->rename_old_path) + patch->base.delta->old_file.path = patch->rename_old_path; + else if (prefixed_old) + patch->base.delta->old_file.path = prefixed_old + old_prefixlen; + else + patch->base.delta->old_file.path = NULL; + + if (patch->rename_new_path) + patch->base.delta->new_file.path = patch->rename_new_path; + else if (prefixed_new) + patch->base.delta->new_file.path = prefixed_new + new_prefixlen; + else + patch->base.delta->new_file.path = NULL; + + if (!patch->base.delta->old_file.path && + !patch->base.delta->new_file.path) + return git_parse_err("git diff header lacks old / new paths"); + + return 0; +} + +static int check_patch(git_patch_parsed *patch) +{ + git_diff_delta *delta = patch->base.delta; + + if (check_filenames(patch) < 0) + return -1; + + if (delta->old_file.path && + delta->status != GIT_DELTA_DELETED && + !delta->new_file.mode) + delta->new_file.mode = delta->old_file.mode; + + if (delta->status == GIT_DELTA_MODIFIED && + !(delta->flags & GIT_DIFF_FLAG_BINARY) && + delta->new_file.mode == delta->old_file.mode && + git_array_size(patch->base.hunks) == 0) + return git_parse_err("patch with no hunks"); + + if (delta->status == GIT_DELTA_ADDED) { + git_oid_clear(&delta->old_file.id, + patch->base.diff_opts.oid_type); + delta->old_file.id_abbrev = 0; + } + + if (delta->status == GIT_DELTA_DELETED) { + git_oid_clear(&delta->new_file.id, + patch->base.diff_opts.oid_type); + delta->new_file.id_abbrev = 0; + } + + return 0; +} + +git_patch_parse_ctx *git_patch_parse_ctx_init( + const char *content, + size_t content_len, + const git_patch_options *opts) +{ + git_patch_parse_ctx *ctx; + git_patch_options default_opts = GIT_PATCH_OPTIONS_INIT; + + if ((ctx = git__calloc(1, sizeof(git_patch_parse_ctx))) == NULL) + return NULL; + + if ((git_parse_ctx_init(&ctx->parse_ctx, content, content_len)) < 0) { + git__free(ctx); + return NULL; + } + + if (opts) + memcpy(&ctx->opts, opts, sizeof(git_patch_options)); + else + memcpy(&ctx->opts, &default_opts, sizeof(git_patch_options)); + + GIT_REFCOUNT_INC(ctx); + return ctx; +} + +static void patch_parse_ctx_free(git_patch_parse_ctx *ctx) +{ + if (!ctx) + return; + + git_parse_ctx_clear(&ctx->parse_ctx); + git__free(ctx); +} + +void git_patch_parse_ctx_free(git_patch_parse_ctx *ctx) +{ + GIT_REFCOUNT_DEC(ctx, patch_parse_ctx_free); +} + +int git_patch_parsed_from_diff(git_patch **out, git_diff *d, size_t idx) +{ + git_diff_parsed *diff = (git_diff_parsed *)d; + git_patch *p; + + if ((p = git_vector_get(&diff->patches, idx)) == NULL) + return -1; + + GIT_REFCOUNT_INC(p); + *out = p; + + return 0; +} + +static void patch_parsed__free(git_patch *p) +{ + git_patch_parsed *patch = (git_patch_parsed *)p; + git_diff_line *line; + size_t i; + + if (!patch) + return; + + git_patch_parse_ctx_free(patch->ctx); + + git__free((char *)patch->base.binary.old_file.data); + git__free((char *)patch->base.binary.new_file.data); + git_array_clear(patch->base.hunks); + git_array_foreach(patch->base.lines, i, line) + git__free((char *) line->content); + git_array_clear(patch->base.lines); + git__free(patch->base.delta); + + git__free(patch->old_prefix); + git__free(patch->new_prefix); + git__free(patch->header_old_path); + git__free(patch->header_new_path); + git__free(patch->rename_old_path); + git__free(patch->rename_new_path); + git__free(patch->old_path); + git__free(patch->new_path); + git__free(patch); +} + +int git_patch_parse( + git_patch **out, + git_patch_parse_ctx *ctx) +{ + git_patch_parsed *patch; + size_t start, used; + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(ctx); + + *out = NULL; + + patch = git__calloc(1, sizeof(git_patch_parsed)); + GIT_ERROR_CHECK_ALLOC(patch); + + patch->ctx = ctx; + GIT_REFCOUNT_INC(patch->ctx); + + patch->base.free_fn = patch_parsed__free; + + patch->base.delta = git__calloc(1, sizeof(git_diff_delta)); + GIT_ERROR_CHECK_ALLOC(patch->base.delta); + + patch->base.delta->status = GIT_DELTA_MODIFIED; + patch->base.delta->nfiles = 2; + + patch->base.diff_opts.oid_type = ctx->opts.oid_type; + + start = ctx->parse_ctx.remain_len; + + if ((error = parse_patch_header(patch, ctx)) < 0 || + (error = parse_patch_body(patch, ctx)) < 0 || + (error = check_patch(patch)) < 0) + goto done; + + used = start - ctx->parse_ctx.remain_len; + ctx->parse_ctx.remain += used; + + patch->base.diff_opts.old_prefix = patch->old_prefix; + patch->base.diff_opts.new_prefix = patch->new_prefix; + patch->base.diff_opts.flags |= GIT_DIFF_SHOW_BINARY; + + GIT_REFCOUNT_INC(&patch->base); + *out = &patch->base; + +done: + if (error < 0) + patch_parsed__free(&patch->base); + + return error; +} + +int git_patch_from_buffer( + git_patch **out, + const char *content, + size_t content_len, + const git_patch_options *opts) +{ + git_patch_parse_ctx *ctx; + int error; + + ctx = git_patch_parse_ctx_init(content, content_len, opts); + GIT_ERROR_CHECK_ALLOC(ctx); + + error = git_patch_parse(out, ctx); + + git_patch_parse_ctx_free(ctx); + return error; +} + diff --git a/src/libgit2/patch_parse.h b/src/libgit2/patch_parse.h new file mode 100644 index 0000000..140629d --- /dev/null +++ b/src/libgit2/patch_parse.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_patch_parse_h__ +#define INCLUDE_patch_parse_h__ + +#include "common.h" + +#include "parse.h" +#include "patch.h" + +typedef struct { + git_refcount rc; + + git_patch_options opts; + + git_parse_ctx parse_ctx; +} git_patch_parse_ctx; + +extern git_patch_parse_ctx *git_patch_parse_ctx_init( + const char *content, + size_t content_len, + const git_patch_options *opts); + +extern void git_patch_parse_ctx_free(git_patch_parse_ctx *ctx); + +/** + * Create a patch for a single file from the contents of a patch buffer. + * + * @param out The patch to be created + * @param contents The contents of a patch file + * @param contents_len The length of the patch file + * @param opts The git_patch_options + * @return 0 on success, <0 on failure. + */ +extern int git_patch_from_buffer( + git_patch **out, + const char *contents, + size_t contents_len, + const git_patch_options *opts); + +extern int git_patch_parse( + git_patch **out, + git_patch_parse_ctx *ctx); + +extern int git_patch_parsed_from_diff(git_patch **, git_diff *, size_t); + +#endif diff --git a/src/libgit2/path.c b/src/libgit2/path.c new file mode 100644 index 0000000..a19340e --- /dev/null +++ b/src/libgit2/path.c @@ -0,0 +1,375 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "path.h" + +#include "repository.h" +#include "fs_path.h" +#include "utf8.h" + +typedef struct { + git_repository *repo; + uint16_t file_mode; + unsigned int flags; +} repository_path_validate_data; + +static int32_t next_hfs_char(const char **in, size_t *len) +{ + while (*len) { + uint32_t codepoint; + int cp_len = git_utf8_iterate(&codepoint, *in, *len); + if (cp_len < 0) + return -1; + + (*in) += cp_len; + (*len) -= cp_len; + + /* these code points are ignored completely */ + switch (codepoint) { + case 0x200c: /* ZERO WIDTH NON-JOINER */ + case 0x200d: /* ZERO WIDTH JOINER */ + case 0x200e: /* LEFT-TO-RIGHT MARK */ + case 0x200f: /* RIGHT-TO-LEFT MARK */ + case 0x202a: /* LEFT-TO-RIGHT EMBEDDING */ + case 0x202b: /* RIGHT-TO-LEFT EMBEDDING */ + case 0x202c: /* POP DIRECTIONAL FORMATTING */ + case 0x202d: /* LEFT-TO-RIGHT OVERRIDE */ + case 0x202e: /* RIGHT-TO-LEFT OVERRIDE */ + case 0x206a: /* INHIBIT SYMMETRIC SWAPPING */ + case 0x206b: /* ACTIVATE SYMMETRIC SWAPPING */ + case 0x206c: /* INHIBIT ARABIC FORM SHAPING */ + case 0x206d: /* ACTIVATE ARABIC FORM SHAPING */ + case 0x206e: /* NATIONAL DIGIT SHAPES */ + case 0x206f: /* NOMINAL DIGIT SHAPES */ + case 0xfeff: /* ZERO WIDTH NO-BREAK SPACE */ + continue; + } + + /* fold into lowercase -- this will only fold characters in + * the ASCII range, which is perfectly fine, because the + * git folder name can only be composed of ascii characters + */ + return git__tolower((int)codepoint); + } + return 0; /* NULL byte -- end of string */ +} + +static bool validate_dotgit_hfs_generic( + const char *path, + size_t len, + const char *needle, + size_t needle_len) +{ + size_t i; + char c; + + if (next_hfs_char(&path, &len) != '.') + return true; + + for (i = 0; i < needle_len; i++) { + c = next_hfs_char(&path, &len); + if (c != needle[i]) + return true; + } + + if (next_hfs_char(&path, &len) != '\0') + return true; + + return false; +} + +static bool validate_dotgit_hfs(const char *path, size_t len) +{ + return validate_dotgit_hfs_generic(path, len, "git", CONST_STRLEN("git")); +} + +GIT_INLINE(bool) validate_dotgit_ntfs( + git_repository *repo, + const char *path, + size_t len) +{ + git_str *reserved = git_repository__reserved_names_win32; + size_t reserved_len = git_repository__reserved_names_win32_len; + size_t start = 0, i; + + if (repo) + git_repository__reserved_names(&reserved, &reserved_len, repo, true); + + for (i = 0; i < reserved_len; i++) { + git_str *r = &reserved[i]; + + if (len >= r->size && + strncasecmp(path, r->ptr, r->size) == 0) { + start = r->size; + break; + } + } + + if (!start) + return true; + + /* + * Reject paths that start with Windows-style directory separators + * (".git\") or NTFS alternate streams (".git:") and could be used + * to write to the ".git" directory on Windows platforms. + */ + if (path[start] == '\\' || path[start] == ':') + return false; + + /* Reject paths like '.git ' or '.git.' */ + for (i = start; i < len; i++) { + if (path[i] != ' ' && path[i] != '.') + return true; + } + + return false; +} + +/* + * Windows paths that end with spaces and/or dots are elided to the + * path without them for backward compatibility. That is to say + * that opening file "foo ", "foo." or even "foo . . ." will all + * map to a filename of "foo". This function identifies spaces and + * dots at the end of a filename, whether the proper end of the + * filename (end of string) or a colon (which would indicate a + * Windows alternate data stream.) + */ +GIT_INLINE(bool) ntfs_end_of_filename(const char *path) +{ + const char *c = path; + + for (;; c++) { + if (*c == '\0' || *c == ':') + return true; + if (*c != ' ' && *c != '.') + return false; + } + + return true; +} + +GIT_INLINE(bool) validate_dotgit_ntfs_generic( + const char *name, + size_t len, + const char *dotgit_name, + size_t dotgit_len, + const char *shortname_pfix) +{ + int i, saw_tilde; + + if (name[0] == '.' && len >= dotgit_len && + !strncasecmp(name + 1, dotgit_name, dotgit_len)) { + return !ntfs_end_of_filename(name + dotgit_len + 1); + } + + /* Detect the basic NTFS shortname with the first six chars */ + if (!strncasecmp(name, dotgit_name, 6) && name[6] == '~' && + name[7] >= '1' && name[7] <= '4') + return !ntfs_end_of_filename(name + 8); + + /* Catch fallback names */ + for (i = 0, saw_tilde = 0; i < 8; i++) { + if (name[i] == '\0') { + return true; + } else if (saw_tilde) { + if (name[i] < '0' || name[i] > '9') + return true; + } else if (name[i] == '~') { + if (name[i+1] < '1' || name[i+1] > '9') + return true; + saw_tilde = 1; + } else if (i >= 6) { + return true; + } else if ((unsigned char)name[i] > 127) { + return true; + } else if (git__tolower(name[i]) != shortname_pfix[i]) { + return true; + } + } + + return !ntfs_end_of_filename(name + i); +} + +/* + * Return the length of the common prefix between str and prefix, comparing them + * case-insensitively (must be ASCII to match). + */ +GIT_INLINE(size_t) common_prefix_icase(const char *str, size_t len, const char *prefix) +{ + size_t count = 0; + + while (len > 0 && tolower(*str) == tolower(*prefix)) { + count++; + str++; + prefix++; + len--; + } + + return count; +} + +static bool validate_repo_component( + const char *component, + size_t len, + void *payload) +{ + repository_path_validate_data *data = (repository_path_validate_data *)payload; + + if (data->flags & GIT_PATH_REJECT_DOT_GIT_HFS) { + if (!validate_dotgit_hfs(component, len)) + return false; + + if (S_ISLNK(data->file_mode) && + git_path_is_gitfile(component, len, GIT_PATH_GITFILE_GITMODULES, GIT_PATH_FS_HFS)) + return false; + } + + if (data->flags & GIT_PATH_REJECT_DOT_GIT_NTFS) { + if (!validate_dotgit_ntfs(data->repo, component, len)) + return false; + + if (S_ISLNK(data->file_mode) && + git_path_is_gitfile(component, len, GIT_PATH_GITFILE_GITMODULES, GIT_PATH_FS_NTFS)) + return false; + } + + /* don't bother rerunning the `.git` test if we ran the HFS or NTFS + * specific tests, they would have already rejected `.git`. + */ + if ((data->flags & GIT_PATH_REJECT_DOT_GIT_HFS) == 0 && + (data->flags & GIT_PATH_REJECT_DOT_GIT_NTFS) == 0 && + (data->flags & GIT_PATH_REJECT_DOT_GIT_LITERAL)) { + if (len >= 4 && + component[0] == '.' && + (component[1] == 'g' || component[1] == 'G') && + (component[2] == 'i' || component[2] == 'I') && + (component[3] == 't' || component[3] == 'T')) { + if (len == 4) + return false; + + if (S_ISLNK(data->file_mode) && + common_prefix_icase(component, len, ".gitmodules") == len) + return false; + } + } + + return true; +} + +GIT_INLINE(unsigned int) dotgit_flags( + git_repository *repo, + unsigned int flags) +{ + int protectHFS = 0, protectNTFS = 1; + int error = 0; + + flags |= GIT_PATH_REJECT_DOT_GIT_LITERAL; + +#ifdef __APPLE__ + protectHFS = 1; +#endif + + if (repo && !protectHFS) + error = git_repository__configmap_lookup(&protectHFS, repo, GIT_CONFIGMAP_PROTECTHFS); + if (!error && protectHFS) + flags |= GIT_PATH_REJECT_DOT_GIT_HFS; + + if (repo) + error = git_repository__configmap_lookup(&protectNTFS, repo, GIT_CONFIGMAP_PROTECTNTFS); + if (!error && protectNTFS) + flags |= GIT_PATH_REJECT_DOT_GIT_NTFS; + + return flags; +} + +GIT_INLINE(unsigned int) length_flags( + git_repository *repo, + unsigned int flags) +{ +#ifdef GIT_WIN32 + int allow = 0; + + if (repo && + git_repository__configmap_lookup(&allow, repo, GIT_CONFIGMAP_LONGPATHS) < 0) + allow = 0; + + if (allow) + flags &= ~GIT_FS_PATH_REJECT_LONG_PATHS; + +#else + GIT_UNUSED(repo); + flags &= ~GIT_FS_PATH_REJECT_LONG_PATHS; +#endif + + return flags; +} + +bool git_path_str_is_valid( + git_repository *repo, + const git_str *path, + uint16_t file_mode, + unsigned int flags) +{ + repository_path_validate_data data = {0}; + + /* Upgrade the ".git" checks based on platform */ + if ((flags & GIT_PATH_REJECT_DOT_GIT)) + flags = dotgit_flags(repo, flags); + + /* Update the length checks based on platform */ + if ((flags & GIT_FS_PATH_REJECT_LONG_PATHS)) + flags = length_flags(repo, flags); + + data.repo = repo; + data.file_mode = file_mode; + data.flags = flags; + + return git_fs_path_str_is_valid_ext(path, flags, NULL, validate_repo_component, NULL, &data); +} + +static const struct { + const char *file; + const char *hash; + size_t filelen; +} gitfiles[] = { + { "gitignore", "gi250a", CONST_STRLEN("gitignore") }, + { "gitmodules", "gi7eba", CONST_STRLEN("gitmodules") }, + { "gitattributes", "gi7d29", CONST_STRLEN("gitattributes") } +}; + +extern int git_path_is_gitfile( + const char *path, + size_t pathlen, + git_path_gitfile gitfile, + git_path_fs fs) +{ + const char *file, *hash; + size_t filelen; + + if (!(gitfile >= GIT_PATH_GITFILE_GITIGNORE && gitfile < ARRAY_SIZE(gitfiles))) { + git_error_set(GIT_ERROR_OS, "invalid gitfile for path validation"); + return -1; + } + + file = gitfiles[gitfile].file; + filelen = gitfiles[gitfile].filelen; + hash = gitfiles[gitfile].hash; + + switch (fs) { + case GIT_PATH_FS_GENERIC: + return !validate_dotgit_ntfs_generic(path, pathlen, file, filelen, hash) || + !validate_dotgit_hfs_generic(path, pathlen, file, filelen); + case GIT_PATH_FS_NTFS: + return !validate_dotgit_ntfs_generic(path, pathlen, file, filelen, hash); + case GIT_PATH_FS_HFS: + return !validate_dotgit_hfs_generic(path, pathlen, file, filelen); + default: + git_error_set(GIT_ERROR_OS, "invalid filesystem for path validation"); + return -1; + } +} + diff --git a/src/libgit2/path.h b/src/libgit2/path.h new file mode 100644 index 0000000..c4a2c42 --- /dev/null +++ b/src/libgit2/path.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_path_h__ +#define INCLUDE_path_h__ + +#include "common.h" + +#include "fs_path.h" +#include + +#define GIT_PATH_REJECT_DOT_GIT (GIT_FS_PATH_REJECT_MAX << 1) +#define GIT_PATH_REJECT_DOT_GIT_LITERAL (GIT_FS_PATH_REJECT_MAX << 2) +#define GIT_PATH_REJECT_DOT_GIT_HFS (GIT_FS_PATH_REJECT_MAX << 3) +#define GIT_PATH_REJECT_DOT_GIT_NTFS (GIT_FS_PATH_REJECT_MAX << 4) + +/* Paths that should never be written into the working directory. */ +#define GIT_PATH_REJECT_WORKDIR_DEFAULTS \ + GIT_FS_PATH_REJECT_FILESYSTEM_DEFAULTS | GIT_PATH_REJECT_DOT_GIT + +/* Paths that should never be written to the index. */ +#define GIT_PATH_REJECT_INDEX_DEFAULTS \ + GIT_FS_PATH_REJECT_TRAVERSAL | GIT_PATH_REJECT_DOT_GIT + +extern bool git_path_str_is_valid( + git_repository *repo, + const git_str *path, + uint16_t file_mode, + unsigned int flags); + +GIT_INLINE(bool) git_path_is_valid( + git_repository *repo, + const char *path, + uint16_t file_mode, + unsigned int flags) +{ + git_str str = GIT_STR_INIT_CONST(path, SIZE_MAX); + return git_path_str_is_valid(repo, &str, file_mode, flags); +} + +GIT_INLINE(int) git_path_validate_str_length( + git_repository *repo, + const git_str *path) +{ + if (!git_path_str_is_valid(repo, path, 0, GIT_FS_PATH_REJECT_LONG_PATHS)) { + if (path->size == SIZE_MAX) + git_error_set(GIT_ERROR_FILESYSTEM, "path too long: '%s'", path->ptr); + else + git_error_set(GIT_ERROR_FILESYSTEM, "path too long: '%.*s'", (int)path->size, path->ptr); + + return -1; + } + + return 0; +} + +GIT_INLINE(int) git_path_validate_length( + git_repository *repo, + const char *path) +{ + git_str str = GIT_STR_INIT_CONST(path, SIZE_MAX); + return git_path_validate_str_length(repo, &str); +} + +#endif diff --git a/src/libgit2/pathspec.c b/src/libgit2/pathspec.c new file mode 100644 index 0000000..3e44643 --- /dev/null +++ b/src/libgit2/pathspec.c @@ -0,0 +1,722 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "pathspec.h" + +#include "git2/pathspec.h" +#include "git2/diff.h" +#include "attr_file.h" +#include "iterator.h" +#include "repository.h" +#include "index.h" +#include "bitvec.h" +#include "diff.h" +#include "wildmatch.h" + +/* what is the common non-wildcard prefix for all items in the pathspec */ +char *git_pathspec_prefix(const git_strarray *pathspec) +{ + git_str prefix = GIT_STR_INIT; + const char *scan; + + if (!pathspec || !pathspec->count || + git_str_common_prefix(&prefix, pathspec->strings, pathspec->count) < 0) + return NULL; + + /* diff prefix will only be leading non-wildcards */ + for (scan = prefix.ptr; *scan; ++scan) { + if (git__iswildcard(*scan) && + (scan == prefix.ptr || (*(scan - 1) != '\\'))) + break; + } + git_str_truncate(&prefix, scan - prefix.ptr); + + if (prefix.size <= 0) { + git_str_dispose(&prefix); + return NULL; + } + + git_str_unescape(&prefix); + + return git_str_detach(&prefix); +} + +/* is there anything in the spec that needs to be filtered on */ +bool git_pathspec_is_empty(const git_strarray *pathspec) +{ + size_t i; + + if (pathspec == NULL) + return true; + + for (i = 0; i < pathspec->count; ++i) { + const char *str = pathspec->strings[i]; + + if (str && str[0]) + return false; + } + + return true; +} + +/* build a vector of fnmatch patterns to evaluate efficiently */ +int git_pathspec__vinit( + git_vector *vspec, const git_strarray *strspec, git_pool *strpool) +{ + size_t i; + + memset(vspec, 0, sizeof(*vspec)); + + if (git_pathspec_is_empty(strspec)) + return 0; + + if (git_vector_init(vspec, strspec->count, NULL) < 0) + return -1; + + for (i = 0; i < strspec->count; ++i) { + int ret; + const char *pattern = strspec->strings[i]; + git_attr_fnmatch *match = git__calloc(1, sizeof(git_attr_fnmatch)); + if (!match) + return -1; + + match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG; + + ret = git_attr_fnmatch__parse(match, strpool, NULL, &pattern); + if (ret == GIT_ENOTFOUND) { + git__free(match); + continue; + } else if (ret < 0) { + git__free(match); + return ret; + } + + if (git_vector_insert(vspec, match) < 0) + return -1; + } + + return 0; +} + +/* free data from the pathspec vector */ +void git_pathspec__vfree(git_vector *vspec) +{ + git_vector_free_deep(vspec); +} + +struct pathspec_match_context { + int wildmatch_flags; + int (*strcomp)(const char *, const char *); + int (*strncomp)(const char *, const char *, size_t); +}; + +static void pathspec_match_context_init( + struct pathspec_match_context *ctxt, + bool disable_fnmatch, + bool casefold) +{ + if (disable_fnmatch) + ctxt->wildmatch_flags = -1; + else if (casefold) + ctxt->wildmatch_flags = WM_CASEFOLD; + else + ctxt->wildmatch_flags = 0; + + if (casefold) { + ctxt->strcomp = git__strcasecmp; + ctxt->strncomp = git__strncasecmp; + } else { + ctxt->strcomp = git__strcmp; + ctxt->strncomp = git__strncmp; + } +} + +static int pathspec_match_one( + const git_attr_fnmatch *match, + struct pathspec_match_context *ctxt, + const char *path) +{ + int result = (match->flags & GIT_ATTR_FNMATCH_MATCH_ALL) ? 0 : WM_NOMATCH; + + if (result == WM_NOMATCH) + result = ctxt->strcomp(match->pattern, path) ? WM_NOMATCH : 0; + + if (ctxt->wildmatch_flags >= 0 && result == WM_NOMATCH) + result = wildmatch(match->pattern, path, ctxt->wildmatch_flags); + + /* if we didn't match, look for exact dirname prefix match */ + if (result == WM_NOMATCH && + (match->flags & GIT_ATTR_FNMATCH_HASWILD) == 0 && + ctxt->strncomp(path, match->pattern, match->length) == 0 && + path[match->length] == '/') + result = 0; + + /* if we didn't match and this is a negative match, check for exact + * match of filename with leading '!' + */ + if (result == WM_NOMATCH && + (match->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0 && + *path == '!' && + ctxt->strncomp(path + 1, match->pattern, match->length) == 0 && + (!path[match->length + 1] || path[match->length + 1] == '/')) + return 1; + + if (result == 0) + return (match->flags & GIT_ATTR_FNMATCH_NEGATIVE) ? 0 : 1; + return -1; +} + +static int git_pathspec__match_at( + size_t *matched_at, + const git_vector *vspec, + struct pathspec_match_context *ctxt, + const char *path0, + const char *path1) +{ + int result = GIT_ENOTFOUND; + size_t i = 0; + const git_attr_fnmatch *match; + + git_vector_foreach(vspec, i, match) { + if (path0 && (result = pathspec_match_one(match, ctxt, path0)) >= 0) + break; + if (path1 && (result = pathspec_match_one(match, ctxt, path1)) >= 0) + break; + } + + *matched_at = i; + return result; +} + +/* match a path against the vectorized pathspec */ +bool git_pathspec__match( + const git_vector *vspec, + const char *path, + bool disable_fnmatch, + bool casefold, + const char **matched_pathspec, + size_t *matched_at) +{ + int result; + size_t pos; + struct pathspec_match_context ctxt; + + if (matched_pathspec) + *matched_pathspec = NULL; + if (matched_at) + *matched_at = GIT_PATHSPEC_NOMATCH; + + if (!vspec || !vspec->length) + return true; + + pathspec_match_context_init(&ctxt, disable_fnmatch, casefold); + + result = git_pathspec__match_at(&pos, vspec, &ctxt, path, NULL); + if (result >= 0) { + if (matched_pathspec) { + const git_attr_fnmatch *match = git_vector_get(vspec, pos); + *matched_pathspec = match->pattern; + } + + if (matched_at) + *matched_at = pos; + } + + return (result > 0); +} + + +int git_pathspec__init(git_pathspec *ps, const git_strarray *paths) +{ + int error = 0; + + memset(ps, 0, sizeof(*ps)); + + ps->prefix = git_pathspec_prefix(paths); + + if ((error = git_pool_init(&ps->pool, 1)) < 0 || + (error = git_pathspec__vinit(&ps->pathspec, paths, &ps->pool)) < 0) + git_pathspec__clear(ps); + + return error; +} + +void git_pathspec__clear(git_pathspec *ps) +{ + git__free(ps->prefix); + git_pathspec__vfree(&ps->pathspec); + git_pool_clear(&ps->pool); + memset(ps, 0, sizeof(*ps)); +} + +int git_pathspec_new(git_pathspec **out, const git_strarray *pathspec) +{ + int error = 0; + git_pathspec *ps = git__malloc(sizeof(git_pathspec)); + GIT_ERROR_CHECK_ALLOC(ps); + + if ((error = git_pathspec__init(ps, pathspec)) < 0) { + git__free(ps); + return error; + } + + GIT_REFCOUNT_INC(ps); + *out = ps; + return 0; +} + +static void pathspec_free(git_pathspec *ps) +{ + git_pathspec__clear(ps); + git__free(ps); +} + +void git_pathspec_free(git_pathspec *ps) +{ + if (!ps) + return; + GIT_REFCOUNT_DEC(ps, pathspec_free); +} + +int git_pathspec_matches_path( + const git_pathspec *ps, uint32_t flags, const char *path) +{ + bool no_fnmatch = (flags & GIT_PATHSPEC_NO_GLOB) != 0; + bool casefold = (flags & GIT_PATHSPEC_IGNORE_CASE) != 0; + + GIT_ASSERT_ARG(ps); + GIT_ASSERT_ARG(path); + + return (0 != git_pathspec__match( + &ps->pathspec, path, no_fnmatch, casefold, NULL, NULL)); +} + +static void pathspec_match_free(git_pathspec_match_list *m) +{ + if (!m) + return; + + git_pathspec_free(m->pathspec); + m->pathspec = NULL; + + git_array_clear(m->matches); + git_array_clear(m->failures); + git_pool_clear(&m->pool); + git__free(m); +} + +static git_pathspec_match_list *pathspec_match_alloc( + git_pathspec *ps, int datatype) +{ + git_pathspec_match_list *m = git__calloc(1, sizeof(git_pathspec_match_list)); + if (!m) + return NULL; + + if (git_pool_init(&m->pool, 1) < 0) + return NULL; + + /* need to keep reference to pathspec and increment refcount because + * failures array stores pointers to the pattern strings of the + * pathspec that had no matches + */ + GIT_REFCOUNT_INC(ps); + m->pathspec = ps; + m->datatype = datatype; + + return m; +} + +GIT_INLINE(size_t) pathspec_mark_pattern(git_bitvec *used, size_t pos) +{ + if (!git_bitvec_get(used, pos)) { + git_bitvec_set(used, pos, true); + return 1; + } + + return 0; +} + +static size_t pathspec_mark_remaining( + git_bitvec *used, + git_vector *patterns, + struct pathspec_match_context *ctxt, + size_t start, + const char *path0, + const char *path1) +{ + size_t count = 0; + + if (path1 == path0) + path1 = NULL; + + for (; start < patterns->length; ++start) { + const git_attr_fnmatch *pat = git_vector_get(patterns, start); + + if (git_bitvec_get(used, start)) + continue; + + if (path0 && pathspec_match_one(pat, ctxt, path0) > 0) + count += pathspec_mark_pattern(used, start); + else if (path1 && pathspec_match_one(pat, ctxt, path1) > 0) + count += pathspec_mark_pattern(used, start); + } + + return count; +} + +static int pathspec_build_failure_array( + git_pathspec_string_array_t *failures, + git_vector *patterns, + git_bitvec *used, + git_pool *pool) +{ + size_t pos; + char **failed; + const git_attr_fnmatch *pat; + + for (pos = 0; pos < patterns->length; ++pos) { + if (git_bitvec_get(used, pos)) + continue; + + if ((failed = git_array_alloc(*failures)) == NULL) + return -1; + + pat = git_vector_get(patterns, pos); + + if ((*failed = git_pool_strdup(pool, pat->pattern)) == NULL) + return -1; + } + + return 0; +} + +static int pathspec_match_from_iterator( + git_pathspec_match_list **out, + git_iterator *iter, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_pathspec_match_list *m = NULL; + const git_index_entry *entry = NULL; + struct pathspec_match_context ctxt; + git_vector *patterns = &ps->pathspec; + bool find_failures = out && (flags & GIT_PATHSPEC_FIND_FAILURES) != 0; + bool failures_only = !out || (flags & GIT_PATHSPEC_FAILURES_ONLY) != 0; + size_t pos, used_ct = 0, found_files = 0; + git_index *index = NULL; + git_bitvec used_patterns; + char **file; + + if (git_bitvec_init(&used_patterns, patterns->length) < 0) + return -1; + + if (out) { + *out = m = pathspec_match_alloc(ps, PATHSPEC_DATATYPE_STRINGS); + GIT_ERROR_CHECK_ALLOC(m); + } + + if ((error = git_iterator_reset_range(iter, ps->prefix, ps->prefix)) < 0) + goto done; + + if (git_iterator_type(iter) == GIT_ITERATOR_WORKDIR && + (error = git_repository_index__weakptr( + &index, git_iterator_owner(iter))) < 0) + goto done; + + pathspec_match_context_init( + &ctxt, (flags & GIT_PATHSPEC_NO_GLOB) != 0, + git_iterator_ignore_case(iter)); + + while (!(error = git_iterator_advance(&entry, iter))) { + /* search for match with entry->path */ + int result = git_pathspec__match_at( + &pos, patterns, &ctxt, entry->path, NULL); + + /* no matches for this path */ + if (result < 0) + continue; + + /* if result was a negative pattern match, then don't list file */ + if (!result) { + used_ct += pathspec_mark_pattern(&used_patterns, pos); + continue; + } + + /* check if path is ignored and untracked */ + if (index != NULL && + git_iterator_current_is_ignored(iter) && + git_index__find_pos(NULL, index, entry->path, 0, GIT_INDEX_STAGE_ANY) < 0) + continue; + + /* mark the matched pattern as used */ + used_ct += pathspec_mark_pattern(&used_patterns, pos); + ++found_files; + + /* if find_failures is on, check if any later patterns also match */ + if (find_failures && used_ct < patterns->length) + used_ct += pathspec_mark_remaining( + &used_patterns, patterns, &ctxt, pos + 1, entry->path, NULL); + + /* if only looking at failures, exit early or just continue */ + if (failures_only || !out) { + if (used_ct == patterns->length) + break; + continue; + } + + /* insert matched path into matches array */ + if ((file = (char **)git_array_alloc(m->matches)) == NULL || + (*file = git_pool_strdup(&m->pool, entry->path)) == NULL) { + error = -1; + goto done; + } + } + + if (error < 0 && error != GIT_ITEROVER) + goto done; + error = 0; + + /* insert patterns that had no matches into failures array */ + if (find_failures && used_ct < patterns->length && + (error = pathspec_build_failure_array( + &m->failures, patterns, &used_patterns, &m->pool)) < 0) + goto done; + + /* if every pattern failed to match, then we have failed */ + if ((flags & GIT_PATHSPEC_NO_MATCH_ERROR) != 0 && !found_files) { + git_error_set(GIT_ERROR_INVALID, "no matching files were found"); + error = GIT_ENOTFOUND; + } + +done: + git_bitvec_free(&used_patterns); + + if (error < 0) { + pathspec_match_free(m); + if (out) *out = NULL; + } + + return error; +} + +static git_iterator_flag_t pathspec_match_iter_flags(uint32_t flags) +{ + git_iterator_flag_t f = 0; + + if ((flags & GIT_PATHSPEC_IGNORE_CASE) != 0) + f |= GIT_ITERATOR_IGNORE_CASE; + else if ((flags & GIT_PATHSPEC_USE_CASE) != 0) + f |= GIT_ITERATOR_DONT_IGNORE_CASE; + + return f; +} + +int git_pathspec_match_workdir( + git_pathspec_match_list **out, + git_repository *repo, + uint32_t flags, + git_pathspec *ps) +{ + git_iterator *iter; + git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT; + int error = 0; + + GIT_ASSERT_ARG(repo); + + iter_opts.flags = pathspec_match_iter_flags(flags); + + if (!(error = git_iterator_for_workdir(&iter, repo, NULL, NULL, &iter_opts))) { + error = pathspec_match_from_iterator(out, iter, flags, ps); + git_iterator_free(iter); + } + + return error; +} + +int git_pathspec_match_index( + git_pathspec_match_list **out, + git_index *index, + uint32_t flags, + git_pathspec *ps) +{ + git_iterator *iter; + git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT; + int error = 0; + + GIT_ASSERT_ARG(index); + + iter_opts.flags = pathspec_match_iter_flags(flags); + + if (!(error = git_iterator_for_index(&iter, git_index_owner(index), index, &iter_opts))) { + error = pathspec_match_from_iterator(out, iter, flags, ps); + git_iterator_free(iter); + } + + return error; +} + +int git_pathspec_match_tree( + git_pathspec_match_list **out, + git_tree *tree, + uint32_t flags, + git_pathspec *ps) +{ + git_iterator *iter; + git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT; + int error = 0; + + GIT_ASSERT_ARG(tree); + + iter_opts.flags = pathspec_match_iter_flags(flags); + + if (!(error = git_iterator_for_tree(&iter, tree, &iter_opts))) { + error = pathspec_match_from_iterator(out, iter, flags, ps); + git_iterator_free(iter); + } + + return error; +} + +int git_pathspec_match_diff( + git_pathspec_match_list **out, + git_diff *diff, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_pathspec_match_list *m = NULL; + struct pathspec_match_context ctxt; + git_vector *patterns = &ps->pathspec; + bool find_failures = out && (flags & GIT_PATHSPEC_FIND_FAILURES) != 0; + bool failures_only = !out || (flags & GIT_PATHSPEC_FAILURES_ONLY) != 0; + size_t i, pos, used_ct = 0, found_deltas = 0; + const git_diff_delta *delta, **match; + git_bitvec used_patterns; + + GIT_ASSERT_ARG(diff); + + if (git_bitvec_init(&used_patterns, patterns->length) < 0) + return -1; + + if (out) { + *out = m = pathspec_match_alloc(ps, PATHSPEC_DATATYPE_DIFF); + GIT_ERROR_CHECK_ALLOC(m); + } + + pathspec_match_context_init( + &ctxt, (flags & GIT_PATHSPEC_NO_GLOB) != 0, + git_diff_is_sorted_icase(diff)); + + git_vector_foreach(&diff->deltas, i, delta) { + /* search for match with delta */ + int result = git_pathspec__match_at( + &pos, patterns, &ctxt, delta->old_file.path, delta->new_file.path); + + /* no matches for this path */ + if (result < 0) + continue; + + /* mark the matched pattern as used */ + used_ct += pathspec_mark_pattern(&used_patterns, pos); + + /* if result was a negative pattern match, then don't list file */ + if (!result) + continue; + + ++found_deltas; + + /* if find_failures is on, check if any later patterns also match */ + if (find_failures && used_ct < patterns->length) + used_ct += pathspec_mark_remaining( + &used_patterns, patterns, &ctxt, pos + 1, + delta->old_file.path, delta->new_file.path); + + /* if only looking at failures, exit early or just continue */ + if (failures_only || !out) { + if (used_ct == patterns->length) + break; + continue; + } + + /* insert matched delta into matches array */ + if (!(match = (const git_diff_delta **)git_array_alloc(m->matches))) { + error = -1; + goto done; + } else { + *match = delta; + } + } + + /* insert patterns that had no matches into failures array */ + if (find_failures && used_ct < patterns->length && + (error = pathspec_build_failure_array( + &m->failures, patterns, &used_patterns, &m->pool)) < 0) + goto done; + + /* if every pattern failed to match, then we have failed */ + if ((flags & GIT_PATHSPEC_NO_MATCH_ERROR) != 0 && !found_deltas) { + git_error_set(GIT_ERROR_INVALID, "no matching deltas were found"); + error = GIT_ENOTFOUND; + } + +done: + git_bitvec_free(&used_patterns); + + if (error < 0) { + pathspec_match_free(m); + if (out) *out = NULL; + } + + return error; +} + +void git_pathspec_match_list_free(git_pathspec_match_list *m) +{ + if (m) + pathspec_match_free(m); +} + +size_t git_pathspec_match_list_entrycount( + const git_pathspec_match_list *m) +{ + return m ? git_array_size(m->matches) : 0; +} + +const char *git_pathspec_match_list_entry( + const git_pathspec_match_list *m, size_t pos) +{ + if (!m || m->datatype != PATHSPEC_DATATYPE_STRINGS || + !git_array_valid_index(m->matches, pos)) + return NULL; + + return *((const char **)git_array_get(m->matches, pos)); +} + +const git_diff_delta *git_pathspec_match_list_diff_entry( + const git_pathspec_match_list *m, size_t pos) +{ + if (!m || m->datatype != PATHSPEC_DATATYPE_DIFF || + !git_array_valid_index(m->matches, pos)) + return NULL; + + return *((const git_diff_delta **)git_array_get(m->matches, pos)); +} + +size_t git_pathspec_match_list_failed_entrycount( + const git_pathspec_match_list *m) +{ + return m ? git_array_size(m->failures) : 0; +} + +const char * git_pathspec_match_list_failed_entry( + const git_pathspec_match_list *m, size_t pos) +{ + char **entry = m ? git_array_get(m->failures, pos) : NULL; + + return entry ? *entry : NULL; +} diff --git a/src/libgit2/pathspec.h b/src/libgit2/pathspec.h new file mode 100644 index 0000000..0256cb9 --- /dev/null +++ b/src/libgit2/pathspec.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_pathspec_h__ +#define INCLUDE_pathspec_h__ + +#include "common.h" + +#include "git2/pathspec.h" +#include "str.h" +#include "vector.h" +#include "pool.h" +#include "array.h" + +/* public compiled pathspec */ +struct git_pathspec { + git_refcount rc; + char *prefix; + git_vector pathspec; + git_pool pool; +}; + +enum { + PATHSPEC_DATATYPE_STRINGS = 0, + PATHSPEC_DATATYPE_DIFF = 1 +}; + +typedef git_array_t(char *) git_pathspec_string_array_t; + +/* public interface to pathspec matching */ +struct git_pathspec_match_list { + git_pathspec *pathspec; + git_array_t(void *) matches; + git_pathspec_string_array_t failures; + git_pool pool; + int datatype; +}; + +/* what is the common non-wildcard prefix for all items in the pathspec */ +extern char *git_pathspec_prefix(const git_strarray *pathspec); + +/* is there anything in the spec that needs to be filtered on */ +extern bool git_pathspec_is_empty(const git_strarray *pathspec); + +/* build a vector of fnmatch patterns to evaluate efficiently */ +extern int git_pathspec__vinit( + git_vector *vspec, const git_strarray *strspec, git_pool *strpool); + +/* free data from the pathspec vector */ +extern void git_pathspec__vfree(git_vector *vspec); + +#define GIT_PATHSPEC_NOMATCH ((size_t)-1) + +/* + * Match a path against the vectorized pathspec. + * The matched pathspec is passed back into the `matched_pathspec` parameter, + * unless it is passed as NULL by the caller. + */ +extern bool git_pathspec__match( + const git_vector *vspec, + const char *path, + bool disable_fnmatch, + bool casefold, + const char **matched_pathspec, + size_t *matched_at); + +/* easy pathspec setup */ + +extern int git_pathspec__init(git_pathspec *ps, const git_strarray *paths); + +extern void git_pathspec__clear(git_pathspec *ps); + +#endif diff --git a/src/libgit2/proxy.c b/src/libgit2/proxy.c new file mode 100644 index 0000000..ef91ad6 --- /dev/null +++ b/src/libgit2/proxy.c @@ -0,0 +1,49 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "proxy.h" + +#include "git2/proxy.h" + +int git_proxy_options_init(git_proxy_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_proxy_options, GIT_PROXY_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_proxy_init_options(git_proxy_options *opts, unsigned int version) +{ + return git_proxy_options_init(opts, version); +} +#endif + +int git_proxy_options_dup(git_proxy_options *tgt, const git_proxy_options *src) +{ + if (!src) { + git_proxy_options_init(tgt, GIT_PROXY_OPTIONS_VERSION); + return 0; + } + + memcpy(tgt, src, sizeof(git_proxy_options)); + if (src->url) { + tgt->url = git__strdup(src->url); + GIT_ERROR_CHECK_ALLOC(tgt->url); + } + + return 0; +} + +void git_proxy_options_dispose(git_proxy_options *opts) +{ + if (!opts) + return; + + git__free((char *) opts->url); + opts->url = NULL; +} diff --git a/src/libgit2/proxy.h b/src/libgit2/proxy.h new file mode 100644 index 0000000..7c0ab59 --- /dev/null +++ b/src/libgit2/proxy.h @@ -0,0 +1,17 @@ +/* +* Copyright (C) the libgit2 contributors. All rights reserved. +* +* This file is part of libgit2, distributed under the GNU GPL v2 with +* a Linking Exception. For full terms see the included COPYING file. +*/ +#ifndef INCLUDE_proxy_h__ +#define INCLUDE_proxy_h__ + +#include "common.h" + +#include "git2/proxy.h" + +extern int git_proxy_options_dup(git_proxy_options *tgt, const git_proxy_options *src); +extern void git_proxy_options_dispose(git_proxy_options *opts); + +#endif diff --git a/src/libgit2/push.c b/src/libgit2/push.c new file mode 100644 index 0000000..8b47abc --- /dev/null +++ b/src/libgit2/push.c @@ -0,0 +1,568 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "push.h" + +#include "git2.h" + +#include "pack.h" +#include "pack-objects.h" +#include "remote.h" +#include "vector.h" +#include "tree.h" + +static int push_spec_rref_cmp(const void *a, const void *b) +{ + const push_spec *push_spec_a = a, *push_spec_b = b; + + return strcmp(push_spec_a->refspec.dst, push_spec_b->refspec.dst); +} + +static int push_status_ref_cmp(const void *a, const void *b) +{ + const push_status *push_status_a = a, *push_status_b = b; + + return strcmp(push_status_a->ref, push_status_b->ref); +} + +int git_push_new(git_push **out, git_remote *remote, const git_push_options *opts) +{ + git_push *p; + + *out = NULL; + + GIT_ERROR_CHECK_VERSION(opts, GIT_PUSH_OPTIONS_VERSION, "git_push_options"); + + p = git__calloc(1, sizeof(*p)); + GIT_ERROR_CHECK_ALLOC(p); + + p->repo = remote->repo; + p->remote = remote; + p->report_status = 1; + p->pb_parallelism = opts ? opts->pb_parallelism : 1; + + if (opts) { + GIT_ERROR_CHECK_VERSION(&opts->callbacks, GIT_REMOTE_CALLBACKS_VERSION, "git_remote_callbacks"); + memcpy(&p->callbacks, &opts->callbacks, sizeof(git_remote_callbacks)); + } + + if (git_vector_init(&p->specs, 0, push_spec_rref_cmp) < 0) { + git__free(p); + return -1; + } + + if (git_vector_init(&p->status, 0, push_status_ref_cmp) < 0) { + git_vector_free(&p->specs); + git__free(p); + return -1; + } + + if (git_vector_init(&p->updates, 0, NULL) < 0) { + git_vector_free(&p->status); + git_vector_free(&p->specs); + git__free(p); + return -1; + } + + *out = p; + return 0; +} + +static void free_refspec(push_spec *spec) +{ + if (spec == NULL) + return; + + git_refspec__dispose(&spec->refspec); + git__free(spec); +} + +static int check_rref(char *ref) +{ + if (git__prefixcmp(ref, "refs/")) { + git_error_set(GIT_ERROR_INVALID, "not a valid reference '%s'", ref); + return -1; + } + + return 0; +} + +static int check_lref(git_push *push, char *ref) +{ + /* lref must be resolvable to an existing object */ + git_object *obj; + int error = git_revparse_single(&obj, push->repo, ref); + git_object_free(obj); + + if (!error) + return 0; + + if (error == GIT_ENOTFOUND) + git_error_set(GIT_ERROR_REFERENCE, + "src refspec '%s' does not match any existing object", ref); + else + git_error_set(GIT_ERROR_INVALID, "not a valid reference '%s'", ref); + return -1; +} + +static int parse_refspec(git_push *push, push_spec **spec, const char *str) +{ + push_spec *s; + + *spec = NULL; + + s = git__calloc(1, sizeof(*s)); + GIT_ERROR_CHECK_ALLOC(s); + + git_oid_clear(&s->loid, push->repo->oid_type); + git_oid_clear(&s->roid, push->repo->oid_type); + + if (git_refspec__parse(&s->refspec, str, false) < 0) { + git_error_set(GIT_ERROR_INVALID, "invalid refspec %s", str); + goto on_error; + } + + if (s->refspec.src && s->refspec.src[0] != '\0' && + check_lref(push, s->refspec.src) < 0) { + goto on_error; + } + + if (check_rref(s->refspec.dst) < 0) + goto on_error; + + *spec = s; + return 0; + +on_error: + free_refspec(s); + return -1; +} + +int git_push_add_refspec(git_push *push, const char *refspec) +{ + push_spec *spec; + + if (parse_refspec(push, &spec, refspec) < 0 || + git_vector_insert(&push->specs, spec) < 0) + return -1; + + return 0; +} + +int git_push_update_tips(git_push *push, const git_remote_callbacks *callbacks) +{ + git_str remote_ref_name = GIT_STR_INIT; + size_t i, j; + git_refspec *fetch_spec; + push_spec *push_spec = NULL; + git_reference *remote_ref; + push_status *status; + int error = 0; + + git_vector_foreach(&push->status, i, status) { + int fire_callback = 1; + + /* Skip unsuccessful updates which have non-empty messages */ + if (status->msg) + continue; + + /* Find the corresponding remote ref */ + fetch_spec = git_remote__matching_refspec(push->remote, status->ref); + if (!fetch_spec) + continue; + + /* Clear the buffer which can be dirty from previous iteration */ + git_str_clear(&remote_ref_name); + + if ((error = git_refspec__transform(&remote_ref_name, fetch_spec, status->ref)) < 0) + goto on_error; + + /* Find matching push ref spec */ + git_vector_foreach(&push->specs, j, push_spec) { + if (!strcmp(push_spec->refspec.dst, status->ref)) + break; + } + + /* Could not find the corresponding push ref spec for this push update */ + if (j == push->specs.length) + continue; + + /* Update the remote ref */ + if (git_oid_is_zero(&push_spec->loid)) { + error = git_reference_lookup(&remote_ref, push->remote->repo, git_str_cstr(&remote_ref_name)); + + if (error >= 0) { + error = git_reference_delete(remote_ref); + git_reference_free(remote_ref); + } + } else { + error = git_reference_create(NULL, push->remote->repo, + git_str_cstr(&remote_ref_name), &push_spec->loid, 1, + "update by push"); + } + + if (error < 0) { + if (error != GIT_ENOTFOUND) + goto on_error; + + git_error_clear(); + fire_callback = 0; + } + + if (fire_callback && callbacks && callbacks->update_tips) { + error = callbacks->update_tips(git_str_cstr(&remote_ref_name), + &push_spec->roid, &push_spec->loid, callbacks->payload); + + if (error < 0) + goto on_error; + } + } + + error = 0; + +on_error: + git_str_dispose(&remote_ref_name); + return error; +} + +/** + * Insert all tags until we find a non-tag object, which is returned + * in `out`. + */ +static int enqueue_tag(git_object **out, git_push *push, git_oid *id) +{ + git_object *obj = NULL, *target = NULL; + int error; + + if ((error = git_object_lookup(&obj, push->repo, id, GIT_OBJECT_TAG)) < 0) + return error; + + while (git_object_type(obj) == GIT_OBJECT_TAG) { + if ((error = git_packbuilder_insert(push->pb, git_object_id(obj), NULL)) < 0) + break; + + if ((error = git_tag_target(&target, (git_tag *) obj)) < 0) + break; + + git_object_free(obj); + obj = target; + } + + if (error < 0) + git_object_free(obj); + else + *out = obj; + + return error; +} + +static int queue_objects(git_push *push) +{ + git_remote_head *head; + push_spec *spec; + git_revwalk *rw; + unsigned int i; + int error = -1; + + if (git_revwalk_new(&rw, push->repo) < 0) + return -1; + + git_revwalk_sorting(rw, GIT_SORT_TIME); + + git_vector_foreach(&push->specs, i, spec) { + git_object_t type; + size_t size; + + if (git_oid_is_zero(&spec->loid)) + /* + * Delete reference on remote side; + * nothing to do here. + */ + continue; + + if (git_oid_equal(&spec->loid, &spec->roid)) + continue; /* up-to-date */ + + if ((error = git_odb_read_header(&size, &type, push->repo->_odb, &spec->loid)) < 0) + goto on_error; + + if (type == GIT_OBJECT_TAG) { + git_object *target; + + if ((error = enqueue_tag(&target, push, &spec->loid)) < 0) + goto on_error; + + if (git_object_type(target) == GIT_OBJECT_COMMIT) { + if ((error = git_revwalk_push(rw, git_object_id(target))) < 0) { + git_object_free(target); + goto on_error; + } + } else { + if ((error = git_packbuilder_insert( + push->pb, git_object_id(target), NULL)) < 0) { + git_object_free(target); + goto on_error; + } + } + git_object_free(target); + } else if ((error = git_revwalk_push(rw, &spec->loid)) < 0) + goto on_error; + + if (!spec->refspec.force) { + git_oid base; + + if (git_oid_is_zero(&spec->roid)) + continue; + + if (!git_odb_exists(push->repo->_odb, &spec->roid)) { + git_error_set(GIT_ERROR_REFERENCE, + "cannot push because a reference that you are trying to update on the remote contains commits that are not present locally."); + error = GIT_ENONFASTFORWARD; + goto on_error; + } + + error = git_merge_base(&base, push->repo, + &spec->loid, &spec->roid); + + if (error == GIT_ENOTFOUND || + (!error && !git_oid_equal(&base, &spec->roid))) { + git_error_set(GIT_ERROR_REFERENCE, + "cannot push non-fastforwardable reference"); + error = GIT_ENONFASTFORWARD; + goto on_error; + } + + if (error < 0) + goto on_error; + } + } + + git_vector_foreach(&push->remote->refs, i, head) { + if (git_oid_is_zero(&head->oid)) + continue; + + if ((error = git_revwalk_hide(rw, &head->oid)) < 0 && + error != GIT_ENOTFOUND && error != GIT_EINVALIDSPEC && error != GIT_EPEEL) + goto on_error; + } + + error = git_packbuilder_insert_walk(push->pb, rw); + +on_error: + git_revwalk_free(rw); + return error; +} + +static int add_update(git_push *push, push_spec *spec) +{ + git_push_update *u = git__calloc(1, sizeof(git_push_update)); + GIT_ERROR_CHECK_ALLOC(u); + + u->src_refname = git__strdup(spec->refspec.src); + GIT_ERROR_CHECK_ALLOC(u->src_refname); + + u->dst_refname = git__strdup(spec->refspec.dst); + GIT_ERROR_CHECK_ALLOC(u->dst_refname); + + git_oid_cpy(&u->src, &spec->roid); + git_oid_cpy(&u->dst, &spec->loid); + + return git_vector_insert(&push->updates, u); +} + +static int calculate_work(git_push *push) +{ + git_remote_head *head; + push_spec *spec; + unsigned int i, j; + + /* Update local and remote oids*/ + + git_vector_foreach(&push->specs, i, spec) { + if (spec->refspec.src && spec->refspec.src[0]!= '\0') { + /* This is a create or update. Local ref must exist. */ + + git_object *obj; + int error = git_revparse_single(&obj, push->repo, spec->refspec.src); + + if (error < 0) { + git_object_free(obj); + git_error_set(GIT_ERROR_REFERENCE, "src refspec %s does not match any", spec->refspec.src); + return -1; + } + + git_oid_cpy(&spec->loid, git_object_id(obj)); + git_object_free(obj); + } + + /* Remote ref may or may not (e.g. during create) already exist. */ + git_vector_foreach(&push->remote->refs, j, head) { + if (!strcmp(spec->refspec.dst, head->name)) { + git_oid_cpy(&spec->roid, &head->oid); + break; + } + } + + if (add_update(push, spec) < 0) + return -1; + } + + return 0; +} + +static int do_push(git_push *push) +{ + int error = 0; + git_transport *transport = push->remote->transport; + git_remote_callbacks *callbacks = &push->callbacks; + + if (!transport->push) { + git_error_set(GIT_ERROR_NET, "remote transport doesn't support push"); + error = -1; + goto on_error; + } + + /* + * A pack-file MUST be sent if either create or update command + * is used, even if the server already has all the necessary + * objects. In this case the client MUST send an empty pack-file. + */ + + if ((error = git_packbuilder_new(&push->pb, push->repo)) < 0) + goto on_error; + + git_packbuilder_set_threads(push->pb, push->pb_parallelism); + + if (callbacks && callbacks->pack_progress) + if ((error = git_packbuilder_set_callbacks(push->pb, callbacks->pack_progress, callbacks->payload)) < 0) + goto on_error; + + if ((error = calculate_work(push)) < 0) + goto on_error; + + if (callbacks && callbacks->push_negotiation && + (error = callbacks->push_negotiation((const git_push_update **) push->updates.contents, + push->updates.length, callbacks->payload)) < 0) + goto on_error; + + if ((error = queue_objects(push)) < 0 || + (error = transport->push(transport, push)) < 0) + goto on_error; + +on_error: + git_packbuilder_free(push->pb); + return error; +} + +static int filter_refs(git_remote *remote) +{ + const git_remote_head **heads; + size_t heads_len, i; + + git_vector_clear(&remote->refs); + + if (git_remote_ls(&heads, &heads_len, remote) < 0) + return -1; + + for (i = 0; i < heads_len; i++) { + if (git_vector_insert(&remote->refs, (void *)heads[i]) < 0) + return -1; + } + + return 0; +} + +int git_push_finish(git_push *push) +{ + int error; + + if (!git_remote_connected(push->remote)) { + git_error_set(GIT_ERROR_NET, "remote is disconnected"); + return -1; + } + + if ((error = filter_refs(push->remote)) < 0 || + (error = do_push(push)) < 0) + return error; + + if (!push->unpack_ok) { + error = -1; + git_error_set(GIT_ERROR_NET, "unpacking the sent packfile failed on the remote"); + } + + return error; +} + +int git_push_status_foreach(git_push *push, + int (*cb)(const char *ref, const char *msg, void *data), + void *data) +{ + push_status *status; + unsigned int i; + + git_vector_foreach(&push->status, i, status) { + int error = cb(status->ref, status->msg, data); + if (error) + return git_error_set_after_callback(error); + } + + return 0; +} + +void git_push_status_free(push_status *status) +{ + if (status == NULL) + return; + + git__free(status->msg); + git__free(status->ref); + git__free(status); +} + +void git_push_free(git_push *push) +{ + push_spec *spec; + push_status *status; + git_push_update *update; + unsigned int i; + + if (push == NULL) + return; + + git_vector_foreach(&push->specs, i, spec) { + free_refspec(spec); + } + git_vector_free(&push->specs); + + git_vector_foreach(&push->status, i, status) { + git_push_status_free(status); + } + git_vector_free(&push->status); + + git_vector_foreach(&push->updates, i, update) { + git__free(update->src_refname); + git__free(update->dst_refname); + git__free(update); + } + git_vector_free(&push->updates); + + git__free(push); +} + +int git_push_options_init(git_push_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_push_options, GIT_PUSH_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_push_init_options(git_push_options *opts, unsigned int version) +{ + return git_push_options_init(opts, version); +} +#endif diff --git a/src/libgit2/push.h b/src/libgit2/push.h new file mode 100644 index 0000000..fc72e84 --- /dev/null +++ b/src/libgit2/push.h @@ -0,0 +1,128 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_push_h__ +#define INCLUDE_push_h__ + +#include "common.h" + +#include "git2.h" +#include "refspec.h" +#include "remote.h" + +typedef struct push_spec { + struct git_refspec refspec; + + git_oid loid; + git_oid roid; +} push_spec; + +typedef struct push_status { + bool ok; + + char *ref; + char *msg; +} push_status; + +struct git_push { + git_repository *repo; + git_packbuilder *pb; + git_remote *remote; + git_vector specs; + git_vector updates; + bool report_status; + + /* report-status */ + bool unpack_ok; + git_vector status; + + /* options */ + unsigned pb_parallelism; + git_remote_callbacks callbacks; +}; + +/** + * Free the given push status object + * + * @param status The push status object + */ +void git_push_status_free(push_status *status); + +/** + * Create a new push object + * + * @param out New push object + * @param remote Remote instance + * @param opts Push options or NULL + * + * @return 0 or an error code + */ +int git_push_new(git_push **out, git_remote *remote, const git_push_options *opts); + +/** + * Add a refspec to be pushed + * + * @param push The push object + * @param refspec Refspec string + * + * @return 0 or an error code + */ +int git_push_add_refspec(git_push *push, const char *refspec); + +/** + * Update remote tips after a push + * + * @param push The push object + * @param callbacks the callbacks to use for this connection + * + * @return 0 or an error code + */ +int git_push_update_tips(git_push *push, const git_remote_callbacks *callbacks); + +/** + * Perform the push + * + * This function will return an error in case of a protocol error or + * the server being unable to unpack the data we sent. + * + * The return value does not reflect whether the server accepted or + * refused any reference updates. Use `git_push_status_foreach()` in + * order to find out which updates were accepted or rejected. + * + * @param push The push object + * + * @return 0 or an error code + */ +int git_push_finish(git_push *push); + +/** + * Invoke callback `cb' on each status entry + * + * For each of the updated references, we receive a status report in the + * form of `ok refs/heads/master` or `ng refs/heads/master `. + * `msg != NULL` means the reference has not been updated for the given + * reason. + * + * Return a non-zero value from the callback to stop the loop. + * + * @param push The push object + * @param cb The callback to call on each object + * @param data The payload passed to the callback + * + * @return 0 on success, non-zero callback return value, or error code + */ +int git_push_status_foreach(git_push *push, + int (*cb)(const char *ref, const char *msg, void *data), + void *data); + +/** + * Free the given push object + * + * @param push The push object + */ +void git_push_free(git_push *push); + +#endif diff --git a/src/libgit2/reader.c b/src/libgit2/reader.c new file mode 100644 index 0000000..df2b280 --- /dev/null +++ b/src/libgit2/reader.c @@ -0,0 +1,269 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "reader.h" + +#include "futils.h" +#include "blob.h" + +#include "git2/tree.h" +#include "git2/blob.h" +#include "git2/index.h" +#include "git2/repository.h" + +/* tree reader */ + +typedef struct { + git_reader reader; + git_tree *tree; +} tree_reader; + +static int tree_reader_read( + git_str *out, + git_oid *out_id, + git_filemode_t *out_filemode, + git_reader *_reader, + const char *filename) +{ + tree_reader *reader = (tree_reader *)_reader; + git_tree_entry *tree_entry = NULL; + git_blob *blob = NULL; + git_object_size_t blobsize; + int error; + + if ((error = git_tree_entry_bypath(&tree_entry, reader->tree, filename)) < 0 || + (error = git_blob_lookup(&blob, git_tree_owner(reader->tree), git_tree_entry_id(tree_entry))) < 0) + goto done; + + blobsize = git_blob_rawsize(blob); + GIT_ERROR_CHECK_BLOBSIZE(blobsize); + + if ((error = git_str_set(out, git_blob_rawcontent(blob), (size_t)blobsize)) < 0) + goto done; + + if (out_id) + git_oid_cpy(out_id, git_tree_entry_id(tree_entry)); + + if (out_filemode) + *out_filemode = git_tree_entry_filemode(tree_entry); + +done: + git_blob_free(blob); + git_tree_entry_free(tree_entry); + return error; +} + +int git_reader_for_tree(git_reader **out, git_tree *tree) +{ + tree_reader *reader; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(tree); + + reader = git__calloc(1, sizeof(tree_reader)); + GIT_ERROR_CHECK_ALLOC(reader); + + reader->reader.read = tree_reader_read; + reader->tree = tree; + + *out = (git_reader *)reader; + return 0; +} + +/* workdir reader */ + +typedef struct { + git_reader reader; + git_repository *repo; + git_index *index; +} workdir_reader; + +static int workdir_reader_read( + git_str *out, + git_oid *out_id, + git_filemode_t *out_filemode, + git_reader *_reader, + const char *filename) +{ + workdir_reader *reader = (workdir_reader *)_reader; + git_str path = GIT_STR_INIT; + struct stat st; + git_filemode_t filemode; + git_filter_list *filters = NULL; + const git_index_entry *idx_entry; + git_oid id; + int error; + + if ((error = git_repository_workdir_path(&path, reader->repo, filename)) < 0) + goto done; + + if ((error = p_lstat(path.ptr, &st)) < 0) { + if (error == -1 && errno == ENOENT) + error = GIT_ENOTFOUND; + + git_error_set(GIT_ERROR_OS, "could not stat '%s'", path.ptr); + goto done; + } + + filemode = git_futils_canonical_mode(st.st_mode); + + /* + * Patch application - for example - uses the filtered version of + * the working directory data to match git. So we will run the + * workdir -> ODB filter on the contents in this workdir reader. + */ + if ((error = git_filter_list_load(&filters, reader->repo, NULL, filename, + GIT_FILTER_TO_ODB, GIT_FILTER_DEFAULT)) < 0) + goto done; + + if ((error = git_filter_list__apply_to_file(out, + filters, reader->repo, path.ptr)) < 0) + goto done; + + if (out_id || reader->index) { + if ((error = git_odb__hash(&id, out->ptr, out->size, GIT_OBJECT_BLOB, reader->repo->oid_type)) < 0) + goto done; + } + + if (reader->index) { + if (!(idx_entry = git_index_get_bypath(reader->index, filename, 0)) || + filemode != idx_entry->mode || + !git_oid_equal(&id, &idx_entry->id)) { + error = GIT_READER_MISMATCH; + goto done; + } + } + + if (out_id) + git_oid_cpy(out_id, &id); + + if (out_filemode) + *out_filemode = filemode; + +done: + git_filter_list_free(filters); + git_str_dispose(&path); + return error; +} + +int git_reader_for_workdir( + git_reader **out, + git_repository *repo, + bool validate_index) +{ + workdir_reader *reader; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + reader = git__calloc(1, sizeof(workdir_reader)); + GIT_ERROR_CHECK_ALLOC(reader); + + reader->reader.read = workdir_reader_read; + reader->repo = repo; + + if (validate_index && + (error = git_repository_index__weakptr(&reader->index, repo)) < 0) { + git__free(reader); + return error; + } + + *out = (git_reader *)reader; + return 0; +} + +/* index reader */ + +typedef struct { + git_reader reader; + git_repository *repo; + git_index *index; +} index_reader; + +static int index_reader_read( + git_str *out, + git_oid *out_id, + git_filemode_t *out_filemode, + git_reader *_reader, + const char *filename) +{ + index_reader *reader = (index_reader *)_reader; + const git_index_entry *entry; + git_blob *blob; + int error; + + if ((entry = git_index_get_bypath(reader->index, filename, 0)) == NULL) + return GIT_ENOTFOUND; + + if ((error = git_blob_lookup(&blob, reader->repo, &entry->id)) < 0) + goto done; + + if (out_id) + git_oid_cpy(out_id, &entry->id); + + if (out_filemode) + *out_filemode = entry->mode; + + error = git_blob__getbuf(out, blob); + +done: + git_blob_free(blob); + return error; +} + +int git_reader_for_index( + git_reader **out, + git_repository *repo, + git_index *index) +{ + index_reader *reader; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + reader = git__calloc(1, sizeof(index_reader)); + GIT_ERROR_CHECK_ALLOC(reader); + + reader->reader.read = index_reader_read; + reader->repo = repo; + + if (index) { + reader->index = index; + } else if ((error = git_repository_index__weakptr(&reader->index, repo)) < 0) { + git__free(reader); + return error; + } + + *out = (git_reader *)reader; + return 0; +} + +/* generic */ + +int git_reader_read( + git_str *out, + git_oid *out_id, + git_filemode_t *out_filemode, + git_reader *reader, + const char *filename) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(reader); + GIT_ASSERT_ARG(filename); + + return reader->read(out, out_id, out_filemode, reader, filename); +} + +void git_reader_free(git_reader *reader) +{ + if (!reader) + return; + + git__free(reader); +} diff --git a/src/libgit2/reader.h b/src/libgit2/reader.h new file mode 100644 index 0000000..b58dc93 --- /dev/null +++ b/src/libgit2/reader.h @@ -0,0 +1,107 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_reader_h__ +#define INCLUDE_reader_h__ + +#include "common.h" + +/* Returned when the workdir does not match the index */ +#define GIT_READER_MISMATCH 1 + +typedef struct git_reader git_reader; + +/* + * The `git_reader` structure is a generic interface for reading the + * contents of a file by its name, and implementations are provided + * for reading out of a tree, the index, and the working directory. + * + * Note that the reader implementation is meant to have a short + * lifecycle and does not increase the refcount of the object that + * it's reading. Callers should ensure that they do not use a + * reader after disposing the underlying object that it reads. + */ +struct git_reader { + int (*read)(git_str *out, git_oid *out_oid, git_filemode_t *mode, git_reader *reader, const char *filename); +}; + +/** + * Create a `git_reader` that will allow random access to the given + * tree. Paths requested via `git_reader_read` will be rooted at this + * tree, callers are not expected to recurse through tree lookups. Thus, + * you can request to read `/src/foo.c` and the tree provided to this + * function will be searched to find another tree named `src`, which + * will then be opened to find `foo.c`. + * + * @param out The reader for the given tree + * @param tree The tree object to read + * @return 0 on success, or an error code < 0 + */ +extern int git_reader_for_tree( + git_reader **out, + git_tree *tree); + +/** + * Create a `git_reader` that will allow random access to the given + * index, or the repository's index. + * + * @param out The reader for the given index + * @param repo The repository containing the index + * @param index The index to read, or NULL to use the repository's index + * @return 0 on success, or an error code < 0 + */ +extern int git_reader_for_index( + git_reader **out, + git_repository *repo, + git_index *index); + +/** + * Create a `git_reader` that will allow random access to the given + * repository's working directory. Note that the contents are read + * in repository format, meaning any workdir -> odb filters are + * applied. + * + * If `validate_index` is set to true, reads of files will hash the + * on-disk contents and ensure that the resulting object ID matches + * the repository's index. This ensures that the working directory + * is unmodified from the index contents. + * + * @param out The reader for the given working directory + * @param repo The repository containing the working directory + * @param validate_index If true, the working directory contents will + * be compared to the index contents during read to ensure that + * the working directory is unmodified. + * @return 0 on success, or an error code < 0 + */ +extern int git_reader_for_workdir( + git_reader **out, + git_repository *repo, + bool validate_index); + +/** + * Read the given filename from the reader and populate the given buffer + * with the contents and the given oid with the object ID. + * + * @param out The buffer to populate with the file contents + * @param out_id The oid to populate with the object ID + * @param reader The reader to read + * @param filename The filename to read from the reader + */ +extern int git_reader_read( + git_str *out, + git_oid *out_id, + git_filemode_t *out_filemode, + git_reader *reader, + const char *filename); + +/** + * Free the given reader and any associated objects. + * + * @param reader The reader to free + */ +extern void git_reader_free(git_reader *reader); + +#endif diff --git a/src/libgit2/rebase.c b/src/libgit2/rebase.c new file mode 100644 index 0000000..77e442e --- /dev/null +++ b/src/libgit2/rebase.c @@ -0,0 +1,1469 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "str.h" +#include "repository.h" +#include "posix.h" +#include "filebuf.h" +#include "commit.h" +#include "merge.h" +#include "array.h" +#include "config.h" +#include "annotated_commit.h" +#include "index.h" + +#include +#include +#include +#include +#include +#include +#include + +#define REBASE_APPLY_DIR "rebase-apply" +#define REBASE_MERGE_DIR "rebase-merge" + +#define HEAD_NAME_FILE "head-name" +#define ORIG_HEAD_FILE "orig-head" +#define HEAD_FILE "head" +#define ONTO_FILE "onto" +#define ONTO_NAME_FILE "onto_name" +#define QUIET_FILE "quiet" +#define INTERACTIVE_FILE "interactive" + +#define MSGNUM_FILE "msgnum" +#define END_FILE "end" +#define CMT_FILE_FMT "cmt.%" PRIuZ +#define CURRENT_FILE "current" +#define REWRITTEN_FILE "rewritten" + +#define ORIG_DETACHED_HEAD "detached HEAD" + +#define NOTES_DEFAULT_REF NULL + +#define REBASE_DIR_MODE 0777 +#define REBASE_FILE_MODE 0666 + +typedef enum { + GIT_REBASE_NONE = 0, + GIT_REBASE_APPLY = 1, + GIT_REBASE_MERGE = 2, + GIT_REBASE_INTERACTIVE = 3 +} git_rebase_t; + +struct git_rebase { + git_repository *repo; + + git_rebase_options options; + + git_rebase_t type; + char *state_path; + + /* Temporary buffer for paths within the state path. */ + git_str state_filename; + + unsigned int head_detached:1, + inmemory:1, + quiet:1, + started:1; + + git_array_t(git_rebase_operation) operations; + size_t current; + + /* Used by in-memory rebase */ + git_index *index; + git_commit *last_commit; + + /* Used by regular (not in-memory) merge-style rebase */ + git_oid orig_head_id; + char *orig_head_name; + + git_oid onto_id; + char *onto_name; +}; + +#define GIT_REBASE_STATE_INIT {0} + +static int rebase_state_type( + git_rebase_t *type_out, + char **path_out, + git_repository *repo) +{ + git_str path = GIT_STR_INIT; + git_str interactive_path = GIT_STR_INIT; + git_rebase_t type = GIT_REBASE_NONE; + + if (git_str_joinpath(&path, repo->gitdir, REBASE_APPLY_DIR) < 0) + return -1; + + if (git_fs_path_isdir(git_str_cstr(&path))) { + type = GIT_REBASE_APPLY; + goto done; + } + + git_str_clear(&path); + if (git_str_joinpath(&path, repo->gitdir, REBASE_MERGE_DIR) < 0) + return -1; + + if (git_fs_path_isdir(git_str_cstr(&path))) { + if (git_str_joinpath(&interactive_path, path.ptr, INTERACTIVE_FILE) < 0) + return -1; + + if (git_fs_path_isfile(interactive_path.ptr)) + type = GIT_REBASE_INTERACTIVE; + else + type = GIT_REBASE_MERGE; + + goto done; + } + +done: + *type_out = type; + + if (type != GIT_REBASE_NONE && path_out) + *path_out = git_str_detach(&path); + + git_str_dispose(&path); + git_str_dispose(&interactive_path); + + return 0; +} + +GIT_INLINE(int) rebase_readfile( + git_str *out, + git_rebase *rebase, + const char *filename) +{ + /* + * `rebase->state_filename` is a temporary buffer to avoid + * unnecessary allocations and copies of `rebase->state_path`. + * At the start and end of this function it always contains the + * contents of `rebase->state_path` itself. + */ + size_t state_path_len = rebase->state_filename.size; + int error; + + git_str_clear(out); + + if ((error = git_str_joinpath(&rebase->state_filename, rebase->state_filename.ptr, filename)) < 0 || + (error = git_futils_readbuffer(out, rebase->state_filename.ptr)) < 0) + goto done; + + git_str_rtrim(out); + +done: + git_str_truncate(&rebase->state_filename, state_path_len); + return error; +} + +GIT_INLINE(int) rebase_readint( + size_t *out, + git_str *asc_out, + git_rebase *rebase, + const char *filename) +{ + int32_t num; + const char *eol; + int error = 0; + + if ((error = rebase_readfile(asc_out, rebase, filename)) < 0) + return error; + + if (git__strntol32(&num, asc_out->ptr, asc_out->size, &eol, 10) < 0 || num < 0 || *eol) { + git_error_set(GIT_ERROR_REBASE, "the file '%s' contains an invalid numeric value", filename); + return -1; + } + + *out = (size_t) num; + + return 0; +} + +GIT_INLINE(int) rebase_readoid( + git_oid *out, + git_str *str_out, + git_rebase *rebase, + const char *filename) +{ + int error; + + if ((error = rebase_readfile(str_out, rebase, filename)) < 0) + return error; + + if (str_out->size != git_oid_hexsize(rebase->repo->oid_type) || + git_oid__fromstr(out, str_out->ptr, rebase->repo->oid_type) < 0) { + git_error_set(GIT_ERROR_REBASE, "the file '%s' contains an invalid object ID", filename); + return -1; + } + + return 0; +} + +static git_rebase_operation *rebase_operation_alloc( + git_rebase *rebase, + git_rebase_operation_t type, + git_oid *id, + const char *exec) +{ + git_rebase_operation *operation; + + GIT_ASSERT_WITH_RETVAL((type == GIT_REBASE_OPERATION_EXEC) == !id, NULL); + GIT_ASSERT_WITH_RETVAL((type == GIT_REBASE_OPERATION_EXEC) == !!exec, NULL); + + if ((operation = git_array_alloc(rebase->operations)) == NULL) + return NULL; + + operation->type = type; + git_oid_cpy((git_oid *)&operation->id, id); + operation->exec = exec; + + return operation; +} + +static int rebase_open_merge(git_rebase *rebase) +{ + git_str buf = GIT_STR_INIT, cmt = GIT_STR_INIT; + git_oid id; + git_rebase_operation *operation; + size_t i, msgnum = 0, end; + int error; + + /* Read 'msgnum' if it exists (otherwise, let msgnum = 0) */ + if ((error = rebase_readint(&msgnum, &buf, rebase, MSGNUM_FILE)) < 0 && + error != GIT_ENOTFOUND) + goto done; + + if (msgnum) { + rebase->started = 1; + rebase->current = msgnum - 1; + } + + /* Read 'end' */ + if ((error = rebase_readint(&end, &buf, rebase, END_FILE)) < 0) + goto done; + + /* Read 'current' if it exists */ + if ((error = rebase_readoid(&id, &buf, rebase, CURRENT_FILE)) < 0 && + error != GIT_ENOTFOUND) + goto done; + + /* Read cmt.* */ + git_array_init_to_size(rebase->operations, end); + GIT_ERROR_CHECK_ARRAY(rebase->operations); + + for (i = 0; i < end; i++) { + git_str_clear(&cmt); + + if ((error = git_str_printf(&cmt, "cmt.%" PRIuZ, (i+1))) < 0 || + (error = rebase_readoid(&id, &buf, rebase, cmt.ptr)) < 0) + goto done; + + operation = rebase_operation_alloc(rebase, GIT_REBASE_OPERATION_PICK, &id, NULL); + GIT_ERROR_CHECK_ALLOC(operation); + } + + /* Read 'onto_name' */ + if ((error = rebase_readfile(&buf, rebase, ONTO_NAME_FILE)) < 0) + goto done; + + rebase->onto_name = git_str_detach(&buf); + +done: + git_str_dispose(&cmt); + git_str_dispose(&buf); + + return error; +} + +static int rebase_alloc(git_rebase **out, const git_rebase_options *rebase_opts) +{ + git_rebase *rebase = git__calloc(1, sizeof(git_rebase)); + GIT_ERROR_CHECK_ALLOC(rebase); + + *out = NULL; + + if (rebase_opts) + memcpy(&rebase->options, rebase_opts, sizeof(git_rebase_options)); + else + git_rebase_options_init(&rebase->options, GIT_REBASE_OPTIONS_VERSION); + + if (rebase_opts && rebase_opts->rewrite_notes_ref) { + rebase->options.rewrite_notes_ref = git__strdup(rebase_opts->rewrite_notes_ref); + GIT_ERROR_CHECK_ALLOC(rebase->options.rewrite_notes_ref); + } + + *out = rebase; + + return 0; +} + +static int rebase_check_versions(const git_rebase_options *given_opts) +{ + GIT_ERROR_CHECK_VERSION(given_opts, GIT_REBASE_OPTIONS_VERSION, "git_rebase_options"); + + if (given_opts) + GIT_ERROR_CHECK_VERSION(&given_opts->checkout_options, GIT_CHECKOUT_OPTIONS_VERSION, "git_checkout_options"); + + return 0; +} + +int git_rebase_open( + git_rebase **out, + git_repository *repo, + const git_rebase_options *given_opts) +{ + git_rebase *rebase; + git_str orig_head_name = GIT_STR_INIT, + orig_head_id = GIT_STR_INIT, + onto_id = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(repo); + + if ((error = rebase_check_versions(given_opts)) < 0) + return error; + + if (rebase_alloc(&rebase, given_opts) < 0) + return -1; + + rebase->repo = repo; + + if ((error = rebase_state_type(&rebase->type, &rebase->state_path, repo)) < 0) + goto done; + + if (rebase->type == GIT_REBASE_NONE) { + git_error_set(GIT_ERROR_REBASE, "there is no rebase in progress"); + error = GIT_ENOTFOUND; + goto done; + } + + if ((error = git_str_puts(&rebase->state_filename, rebase->state_path)) < 0) + goto done; + + if ((error = rebase_readfile(&orig_head_name, rebase, HEAD_NAME_FILE)) < 0) + goto done; + + git_str_rtrim(&orig_head_name); + + if (strcmp(ORIG_DETACHED_HEAD, orig_head_name.ptr) == 0) + rebase->head_detached = 1; + + if ((error = rebase_readoid(&rebase->orig_head_id, &orig_head_id, rebase, ORIG_HEAD_FILE)) < 0) { + /* Previous versions of git.git used 'head' here; support that. */ + if (error == GIT_ENOTFOUND) + error = rebase_readoid(&rebase->orig_head_id, &orig_head_id, rebase, HEAD_FILE); + + if (error < 0) + goto done; + } + + if ((error = rebase_readoid(&rebase->onto_id, &onto_id, rebase, ONTO_FILE)) < 0) + goto done; + + if (!rebase->head_detached) + rebase->orig_head_name = git_str_detach(&orig_head_name); + + switch (rebase->type) { + case GIT_REBASE_INTERACTIVE: + git_error_set(GIT_ERROR_REBASE, "interactive rebase is not supported"); + error = -1; + break; + case GIT_REBASE_MERGE: + error = rebase_open_merge(rebase); + break; + case GIT_REBASE_APPLY: + git_error_set(GIT_ERROR_REBASE, "patch application rebase is not supported"); + error = -1; + break; + default: + abort(); + } + +done: + if (error == 0) + *out = rebase; + else + git_rebase_free(rebase); + + git_str_dispose(&orig_head_name); + git_str_dispose(&orig_head_id); + git_str_dispose(&onto_id); + return error; +} + +static int rebase_cleanup(git_rebase *rebase) +{ + if (!rebase || rebase->inmemory) + return 0; + + return git_fs_path_isdir(rebase->state_path) ? + git_futils_rmdir_r(rebase->state_path, NULL, GIT_RMDIR_REMOVE_FILES) : + 0; +} + +static int rebase_setupfile(git_rebase *rebase, const char *filename, int flags, const char *fmt, ...) +{ + git_str path = GIT_STR_INIT, + contents = GIT_STR_INIT; + va_list ap; + int error; + + va_start(ap, fmt); + git_str_vprintf(&contents, fmt, ap); + va_end(ap); + + if ((error = git_str_joinpath(&path, rebase->state_path, filename)) == 0) + error = git_futils_writebuffer(&contents, path.ptr, flags, REBASE_FILE_MODE); + + git_str_dispose(&path); + git_str_dispose(&contents); + + return error; +} + +static const char *rebase_onto_name(const git_annotated_commit *onto) +{ + if (onto->ref_name && git__strncmp(onto->ref_name, "refs/heads/", 11) == 0) + return onto->ref_name + 11; + else if (onto->ref_name) + return onto->ref_name; + else + return onto->id_str; +} + +static int rebase_setupfiles_merge(git_rebase *rebase) +{ + git_str commit_filename = GIT_STR_INIT; + char id_str[GIT_OID_MAX_HEXSIZE + 1]; + git_rebase_operation *operation; + size_t i; + int error = 0; + + if ((error = rebase_setupfile(rebase, END_FILE, 0, "%" PRIuZ "\n", git_array_size(rebase->operations))) < 0 || + (error = rebase_setupfile(rebase, ONTO_NAME_FILE, 0, "%s\n", rebase->onto_name)) < 0) + goto done; + + for (i = 0; i < git_array_size(rebase->operations); i++) { + operation = git_array_get(rebase->operations, i); + + git_str_clear(&commit_filename); + git_str_printf(&commit_filename, CMT_FILE_FMT, i+1); + + git_oid_tostr(id_str, GIT_OID_MAX_HEXSIZE + 1, &operation->id); + + if ((error = rebase_setupfile(rebase, commit_filename.ptr, 0, "%s\n", id_str)) < 0) + goto done; + } + +done: + git_str_dispose(&commit_filename); + return error; +} + +static int rebase_setupfiles(git_rebase *rebase) +{ + char onto[GIT_OID_MAX_HEXSIZE + 1], orig_head[GIT_OID_MAX_HEXSIZE + 1]; + const char *orig_head_name; + + git_oid_tostr(onto, GIT_OID_MAX_HEXSIZE + 1, &rebase->onto_id); + git_oid_tostr(orig_head, GIT_OID_MAX_HEXSIZE + 1, &rebase->orig_head_id); + + if (p_mkdir(rebase->state_path, REBASE_DIR_MODE) < 0) { + git_error_set(GIT_ERROR_OS, "failed to create rebase directory '%s'", rebase->state_path); + return -1; + } + + orig_head_name = rebase->head_detached ? ORIG_DETACHED_HEAD : + rebase->orig_head_name; + + if (git_repository__set_orig_head(rebase->repo, &rebase->orig_head_id) < 0 || + rebase_setupfile(rebase, HEAD_NAME_FILE, 0, "%s\n", orig_head_name) < 0 || + rebase_setupfile(rebase, ONTO_FILE, 0, "%s\n", onto) < 0 || + rebase_setupfile(rebase, ORIG_HEAD_FILE, 0, "%s\n", orig_head) < 0 || + rebase_setupfile(rebase, QUIET_FILE, 0, rebase->quiet ? "t\n" : "\n") < 0) + return -1; + + return rebase_setupfiles_merge(rebase); +} + +int git_rebase_options_init(git_rebase_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_rebase_options, GIT_REBASE_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_rebase_init_options(git_rebase_options *opts, unsigned int version) +{ + return git_rebase_options_init(opts, version); +} +#endif + +static int rebase_ensure_not_in_progress(git_repository *repo) +{ + int error; + git_rebase_t type; + + if ((error = rebase_state_type(&type, NULL, repo)) < 0) + return error; + + if (type != GIT_REBASE_NONE) { + git_error_set(GIT_ERROR_REBASE, "there is an existing rebase in progress"); + return -1; + } + + return 0; +} + +static int rebase_ensure_not_dirty( + git_repository *repo, + bool check_index, + bool check_workdir, + int fail_with) +{ + git_tree *head = NULL; + git_index *index = NULL; + git_diff *diff = NULL; + int error = 0; + + if (check_index) { + if ((error = git_repository_head_tree(&head, repo)) < 0 || + (error = git_repository_index(&index, repo)) < 0 || + (error = git_diff_tree_to_index(&diff, repo, head, index, NULL)) < 0) + goto done; + + if (git_diff_num_deltas(diff) > 0) { + git_error_set(GIT_ERROR_REBASE, "uncommitted changes exist in index"); + error = fail_with; + goto done; + } + + git_diff_free(diff); + diff = NULL; + } + + if (check_workdir) { + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.ignore_submodules = GIT_SUBMODULE_IGNORE_UNTRACKED; + if ((error = git_diff_index_to_workdir(&diff, repo, index, &diff_opts)) < 0) + goto done; + + if (git_diff_num_deltas(diff) > 0) { + git_error_set(GIT_ERROR_REBASE, "unstaged changes exist in workdir"); + error = fail_with; + goto done; + } + } + +done: + git_diff_free(diff); + git_index_free(index); + git_tree_free(head); + + return error; +} + +static int rebase_init_operations( + git_rebase *rebase, + git_repository *repo, + const git_annotated_commit *branch, + const git_annotated_commit *upstream, + const git_annotated_commit *onto) +{ + git_revwalk *revwalk = NULL; + git_commit *commit; + git_oid id; + bool merge; + git_rebase_operation *operation; + int error; + + if (!upstream) + upstream = onto; + + if ((error = git_revwalk_new(&revwalk, rebase->repo)) < 0 || + (error = git_revwalk_push(revwalk, git_annotated_commit_id(branch))) < 0 || + (error = git_revwalk_hide(revwalk, git_annotated_commit_id(upstream))) < 0) + goto done; + + git_revwalk_sorting(revwalk, GIT_SORT_REVERSE); + + while ((error = git_revwalk_next(&id, revwalk)) == 0) { + if ((error = git_commit_lookup(&commit, repo, &id)) < 0) + goto done; + + merge = (git_commit_parentcount(commit) > 1); + git_commit_free(commit); + + if (merge) + continue; + + operation = rebase_operation_alloc(rebase, GIT_REBASE_OPERATION_PICK, &id, NULL); + GIT_ERROR_CHECK_ALLOC(operation); + } + + error = 0; + +done: + git_revwalk_free(revwalk); + return error; +} + +static int rebase_init_merge( + git_rebase *rebase, + git_repository *repo, + const git_annotated_commit *branch, + const git_annotated_commit *upstream, + const git_annotated_commit *onto) +{ + git_reference *head_ref = NULL; + git_commit *onto_commit = NULL; + git_str reflog = GIT_STR_INIT; + git_str state_path = GIT_STR_INIT; + int error; + + GIT_UNUSED(upstream); + + if ((error = git_str_joinpath(&state_path, repo->gitdir, REBASE_MERGE_DIR)) < 0 || + (error = git_str_put(&rebase->state_filename, state_path.ptr, state_path.size)) < 0) + goto done; + + rebase->state_path = git_str_detach(&state_path); + GIT_ERROR_CHECK_ALLOC(rebase->state_path); + + if (branch->ref_name && strcmp(branch->ref_name, "HEAD")) { + rebase->orig_head_name = git__strdup(branch->ref_name); + GIT_ERROR_CHECK_ALLOC(rebase->orig_head_name); + } else { + rebase->head_detached = 1; + } + + rebase->onto_name = git__strdup(rebase_onto_name(onto)); + GIT_ERROR_CHECK_ALLOC(rebase->onto_name); + + rebase->quiet = rebase->options.quiet; + + git_oid_cpy(&rebase->orig_head_id, git_annotated_commit_id(branch)); + git_oid_cpy(&rebase->onto_id, git_annotated_commit_id(onto)); + + if ((error = rebase_setupfiles(rebase)) < 0 || + (error = git_str_printf(&reflog, + "rebase: checkout %s", rebase_onto_name(onto))) < 0 || + (error = git_commit_lookup( + &onto_commit, repo, git_annotated_commit_id(onto))) < 0 || + (error = git_checkout_tree(repo, + (git_object *)onto_commit, &rebase->options.checkout_options)) < 0 || + (error = git_reference_create(&head_ref, repo, GIT_HEAD_FILE, + git_annotated_commit_id(onto), 1, reflog.ptr)) < 0) + goto done; + +done: + git_reference_free(head_ref); + git_commit_free(onto_commit); + git_str_dispose(&reflog); + git_str_dispose(&state_path); + + return error; +} + +static int rebase_init_inmemory( + git_rebase *rebase, + git_repository *repo, + const git_annotated_commit *branch, + const git_annotated_commit *upstream, + const git_annotated_commit *onto) +{ + GIT_UNUSED(branch); + GIT_UNUSED(upstream); + + return git_commit_lookup( + &rebase->last_commit, repo, git_annotated_commit_id(onto)); +} + +int git_rebase_init( + git_rebase **out, + git_repository *repo, + const git_annotated_commit *branch, + const git_annotated_commit *upstream, + const git_annotated_commit *onto, + const git_rebase_options *given_opts) +{ + git_rebase *rebase = NULL; + git_annotated_commit *head_branch = NULL; + git_reference *head_ref = NULL; + bool inmemory = (given_opts && given_opts->inmemory); + int error; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(upstream || onto); + + *out = NULL; + + if (!onto) + onto = upstream; + + if ((error = rebase_check_versions(given_opts)) < 0) + goto done; + + if (!inmemory) { + if ((error = git_repository__ensure_not_bare(repo, "rebase")) < 0 || + (error = rebase_ensure_not_in_progress(repo)) < 0 || + (error = rebase_ensure_not_dirty(repo, true, true, GIT_ERROR)) < 0) + goto done; + } + + if (!branch) { + if ((error = git_repository_head(&head_ref, repo)) < 0 || + (error = git_annotated_commit_from_ref(&head_branch, repo, head_ref)) < 0) + goto done; + + branch = head_branch; + } + + if (rebase_alloc(&rebase, given_opts) < 0) + return -1; + + rebase->repo = repo; + rebase->inmemory = inmemory; + rebase->type = GIT_REBASE_MERGE; + + if ((error = rebase_init_operations(rebase, repo, branch, upstream, onto)) < 0) + goto done; + + if (inmemory) + error = rebase_init_inmemory(rebase, repo, branch, upstream, onto); + else + error = rebase_init_merge(rebase, repo, branch ,upstream, onto); + + if (error == 0) + *out = rebase; + +done: + git_reference_free(head_ref); + git_annotated_commit_free(head_branch); + + if (error < 0) { + rebase_cleanup(rebase); + git_rebase_free(rebase); + } + + return error; +} + +static void normalize_checkout_options_for_apply( + git_checkout_options *checkout_opts, + git_rebase *rebase, + git_commit *current_commit) +{ + memcpy(checkout_opts, &rebase->options.checkout_options, sizeof(git_checkout_options)); + + if (!checkout_opts->ancestor_label) + checkout_opts->ancestor_label = "ancestor"; + + if (rebase->type == GIT_REBASE_MERGE) { + if (!checkout_opts->our_label) + checkout_opts->our_label = rebase->onto_name; + + if (!checkout_opts->their_label) + checkout_opts->their_label = git_commit_summary(current_commit); + } else { + abort(); + } +} + +GIT_INLINE(int) rebase_movenext(git_rebase *rebase) +{ + size_t next = rebase->started ? rebase->current + 1 : 0; + + if (next == git_array_size(rebase->operations)) + return GIT_ITEROVER; + + rebase->started = 1; + rebase->current = next; + + return 0; +} + +static int rebase_next_merge( + git_rebase_operation **out, + git_rebase *rebase) +{ + git_str path = GIT_STR_INIT; + git_commit *current_commit = NULL, *parent_commit = NULL; + git_tree *current_tree = NULL, *head_tree = NULL, *parent_tree = NULL; + git_index *index = NULL; + git_indexwriter indexwriter = GIT_INDEXWRITER_INIT; + git_rebase_operation *operation; + git_checkout_options checkout_opts; + char current_idstr[GIT_OID_MAX_HEXSIZE + 1]; + unsigned int parent_count; + int error; + + *out = NULL; + + operation = git_array_get(rebase->operations, rebase->current); + + if ((error = git_commit_lookup(¤t_commit, rebase->repo, &operation->id)) < 0 || + (error = git_commit_tree(¤t_tree, current_commit)) < 0 || + (error = git_repository_head_tree(&head_tree, rebase->repo)) < 0) + goto done; + + if ((parent_count = git_commit_parentcount(current_commit)) > 1) { + git_error_set(GIT_ERROR_REBASE, "cannot rebase a merge commit"); + error = -1; + goto done; + } else if (parent_count) { + if ((error = git_commit_parent(&parent_commit, current_commit, 0)) < 0 || + (error = git_commit_tree(&parent_tree, parent_commit)) < 0) + goto done; + } + + git_oid_tostr(current_idstr, GIT_OID_MAX_HEXSIZE + 1, &operation->id); + + normalize_checkout_options_for_apply(&checkout_opts, rebase, current_commit); + + if ((error = git_indexwriter_init_for_operation(&indexwriter, rebase->repo, &checkout_opts.checkout_strategy)) < 0 || + (error = rebase_setupfile(rebase, MSGNUM_FILE, 0, "%" PRIuZ "\n", rebase->current+1)) < 0 || + (error = rebase_setupfile(rebase, CURRENT_FILE, 0, "%s\n", current_idstr)) < 0 || + (error = git_merge_trees(&index, rebase->repo, parent_tree, head_tree, current_tree, &rebase->options.merge_options)) < 0 || + (error = git_merge__check_result(rebase->repo, index)) < 0 || + (error = git_checkout_index(rebase->repo, index, &checkout_opts)) < 0 || + (error = git_indexwriter_commit(&indexwriter)) < 0) + goto done; + + *out = operation; + +done: + git_indexwriter_cleanup(&indexwriter); + git_index_free(index); + git_tree_free(current_tree); + git_tree_free(head_tree); + git_tree_free(parent_tree); + git_commit_free(parent_commit); + git_commit_free(current_commit); + git_str_dispose(&path); + + return error; +} + +static int rebase_next_inmemory( + git_rebase_operation **out, + git_rebase *rebase) +{ + git_commit *current_commit = NULL, *parent_commit = NULL; + git_tree *current_tree = NULL, *head_tree = NULL, *parent_tree = NULL; + git_rebase_operation *operation; + git_index *index = NULL; + unsigned int parent_count; + int error; + + *out = NULL; + + operation = git_array_get(rebase->operations, rebase->current); + + if ((error = git_commit_lookup(¤t_commit, rebase->repo, &operation->id)) < 0 || + (error = git_commit_tree(¤t_tree, current_commit)) < 0) + goto done; + + if ((parent_count = git_commit_parentcount(current_commit)) > 1) { + git_error_set(GIT_ERROR_REBASE, "cannot rebase a merge commit"); + error = -1; + goto done; + } else if (parent_count) { + if ((error = git_commit_parent(&parent_commit, current_commit, 0)) < 0 || + (error = git_commit_tree(&parent_tree, parent_commit)) < 0) + goto done; + } + + if ((error = git_commit_tree(&head_tree, rebase->last_commit)) < 0 || + (error = git_merge_trees(&index, rebase->repo, parent_tree, head_tree, current_tree, &rebase->options.merge_options)) < 0) + goto done; + + if (!rebase->index) { + rebase->index = index; + index = NULL; + } else { + if ((error = git_index_read_index(rebase->index, index)) < 0) + goto done; + } + + *out = operation; + +done: + git_commit_free(current_commit); + git_commit_free(parent_commit); + git_tree_free(current_tree); + git_tree_free(head_tree); + git_tree_free(parent_tree); + git_index_free(index); + + return error; +} + +int git_rebase_next( + git_rebase_operation **out, + git_rebase *rebase) +{ + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(rebase); + + if ((error = rebase_movenext(rebase)) < 0) + return error; + + if (rebase->inmemory) + error = rebase_next_inmemory(out, rebase); + else if (rebase->type == GIT_REBASE_MERGE) + error = rebase_next_merge(out, rebase); + else + abort(); + + return error; +} + +int git_rebase_inmemory_index( + git_index **out, + git_rebase *rebase) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(rebase); + GIT_ASSERT_ARG(rebase->index); + + GIT_REFCOUNT_INC(rebase->index); + *out = rebase->index; + + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +static int create_signed( + git_oid *out, + git_rebase *rebase, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + git_tree *tree, + size_t parent_count, + const git_commit **parents) +{ + git_str commit_content = GIT_STR_INIT; + git_buf commit_signature = { NULL, 0, 0 }, + signature_field = { NULL, 0, 0 }; + int error; + + git_error_clear(); + + if ((error = git_commit__create_buffer(&commit_content, + rebase->repo, author, committer, message_encoding, + message, tree, parent_count, parents)) < 0) + goto done; + + error = rebase->options.signing_cb(&commit_signature, + &signature_field, commit_content.ptr, + rebase->options.payload); + + if (error) { + if (error != GIT_PASSTHROUGH) + git_error_set_after_callback_function(error, "signing_cb"); + + goto done; + } + + error = git_commit_create_with_signature(out, rebase->repo, + commit_content.ptr, + commit_signature.size > 0 ? commit_signature.ptr : NULL, + signature_field.size > 0 ? signature_field.ptr : NULL); + +done: + git_buf_dispose(&commit_signature); + git_buf_dispose(&signature_field); + git_str_dispose(&commit_content); + return error; +} +#endif + +static int rebase_commit__create( + git_commit **out, + git_rebase *rebase, + git_index *index, + git_commit *parent_commit, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message) +{ + git_rebase_operation *operation; + git_commit *current_commit = NULL, *commit = NULL; + git_tree *parent_tree = NULL, *tree = NULL; + git_oid tree_id, commit_id; + int error; + + operation = git_array_get(rebase->operations, rebase->current); + + if (git_index_has_conflicts(index)) { + git_error_set(GIT_ERROR_REBASE, "conflicts have not been resolved"); + error = GIT_EUNMERGED; + goto done; + } + + if ((error = git_commit_lookup(¤t_commit, rebase->repo, &operation->id)) < 0 || + (error = git_commit_tree(&parent_tree, parent_commit)) < 0 || + (error = git_index_write_tree_to(&tree_id, index, rebase->repo)) < 0 || + (error = git_tree_lookup(&tree, rebase->repo, &tree_id)) < 0) + goto done; + + if (git_oid_equal(&tree_id, git_tree_id(parent_tree))) { + git_error_set(GIT_ERROR_REBASE, "this patch has already been applied"); + error = GIT_EAPPLIED; + goto done; + } + + if (!author) + author = git_commit_author(current_commit); + + if (!message) { + message_encoding = git_commit_message_encoding(current_commit); + message = git_commit_message(current_commit); + } + + git_error_clear(); + error = GIT_PASSTHROUGH; + + if (rebase->options.commit_create_cb) { + error = rebase->options.commit_create_cb(&commit_id, + author, committer, message_encoding, message, + tree, 1, (const git_commit **)&parent_commit, + rebase->options.payload); + + git_error_set_after_callback_function(error, + "commit_create_cb"); + } +#ifndef GIT_DEPRECATE_HARD + else if (rebase->options.signing_cb) { + error = create_signed(&commit_id, rebase, author, + committer, message_encoding, message, tree, + 1, (const git_commit **)&parent_commit); + } +#endif + + if (error == GIT_PASSTHROUGH) + error = git_commit_create(&commit_id, rebase->repo, NULL, + author, committer, message_encoding, message, + tree, 1, (const git_commit **)&parent_commit); + + if (error) + goto done; + + if ((error = git_commit_lookup(&commit, rebase->repo, &commit_id)) < 0) + goto done; + + *out = commit; + +done: + if (error < 0) + git_commit_free(commit); + + git_commit_free(current_commit); + git_tree_free(parent_tree); + git_tree_free(tree); + + return error; +} + +static int rebase_commit_merge( + git_oid *commit_id, + git_rebase *rebase, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message) +{ + git_rebase_operation *operation; + git_reference *head = NULL; + git_commit *head_commit = NULL, *commit = NULL; + git_index *index = NULL; + char old_idstr[GIT_OID_MAX_HEXSIZE + 1], new_idstr[GIT_OID_MAX_HEXSIZE + 1]; + int error; + + operation = git_array_get(rebase->operations, rebase->current); + GIT_ASSERT(operation); + + if ((error = rebase_ensure_not_dirty(rebase->repo, false, true, GIT_EUNMERGED)) < 0 || + (error = git_repository_head(&head, rebase->repo)) < 0 || + (error = git_reference_peel((git_object **)&head_commit, head, GIT_OBJECT_COMMIT)) < 0 || + (error = git_repository_index(&index, rebase->repo)) < 0 || + (error = rebase_commit__create(&commit, rebase, index, head_commit, + author, committer, message_encoding, message)) < 0 || + (error = git_reference__update_for_commit( + rebase->repo, NULL, "HEAD", git_commit_id(commit), "rebase")) < 0) + goto done; + + git_oid_tostr(old_idstr, GIT_OID_MAX_HEXSIZE + 1, &operation->id); + git_oid_tostr(new_idstr, GIT_OID_MAX_HEXSIZE + 1, git_commit_id(commit)); + + if ((error = rebase_setupfile(rebase, REWRITTEN_FILE, O_CREAT|O_WRONLY|O_APPEND, + "%s %s\n", old_idstr, new_idstr)) < 0) + goto done; + + git_oid_cpy(commit_id, git_commit_id(commit)); + +done: + git_index_free(index); + git_reference_free(head); + git_commit_free(head_commit); + git_commit_free(commit); + return error; +} + +static int rebase_commit_inmemory( + git_oid *commit_id, + git_rebase *rebase, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message) +{ + git_commit *commit = NULL; + int error = 0; + + GIT_ASSERT_ARG(rebase->index); + GIT_ASSERT_ARG(rebase->last_commit); + GIT_ASSERT_ARG(rebase->current < rebase->operations.size); + + if ((error = rebase_commit__create(&commit, rebase, rebase->index, + rebase->last_commit, author, committer, message_encoding, message)) < 0) + goto done; + + git_commit_free(rebase->last_commit); + rebase->last_commit = commit; + + git_oid_cpy(commit_id, git_commit_id(commit)); + +done: + if (error < 0) + git_commit_free(commit); + + return error; +} + +int git_rebase_commit( + git_oid *id, + git_rebase *rebase, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message) +{ + int error; + + GIT_ASSERT_ARG(rebase); + GIT_ASSERT_ARG(committer); + + if (rebase->inmemory) + error = rebase_commit_inmemory( + id, rebase, author, committer, message_encoding, message); + else if (rebase->type == GIT_REBASE_MERGE) + error = rebase_commit_merge( + id, rebase, author, committer, message_encoding, message); + else + abort(); + + return error; +} + +int git_rebase_abort(git_rebase *rebase) +{ + git_reference *orig_head_ref = NULL; + git_commit *orig_head_commit = NULL; + int error; + + GIT_ASSERT_ARG(rebase); + + if (rebase->inmemory) + return 0; + + error = rebase->head_detached ? + git_reference_create(&orig_head_ref, rebase->repo, GIT_HEAD_FILE, + &rebase->orig_head_id, 1, "rebase: aborting") : + git_reference_symbolic_create( + &orig_head_ref, rebase->repo, GIT_HEAD_FILE, rebase->orig_head_name, 1, + "rebase: aborting"); + + if (error < 0) + goto done; + + if ((error = git_commit_lookup( + &orig_head_commit, rebase->repo, &rebase->orig_head_id)) < 0 || + (error = git_reset(rebase->repo, (git_object *)orig_head_commit, + GIT_RESET_HARD, &rebase->options.checkout_options)) < 0) + goto done; + + error = rebase_cleanup(rebase); + +done: + git_commit_free(orig_head_commit); + git_reference_free(orig_head_ref); + + return error; +} + +static int notes_ref_lookup(git_str *out, git_rebase *rebase) +{ + git_config *config = NULL; + int do_rewrite, error; + + if (rebase->options.rewrite_notes_ref) { + git_str_attach_notowned(out, + rebase->options.rewrite_notes_ref, + strlen(rebase->options.rewrite_notes_ref)); + return 0; + } + + if ((error = git_repository_config(&config, rebase->repo)) < 0 || + (error = git_config_get_bool(&do_rewrite, config, "notes.rewrite.rebase")) < 0) { + + if (error != GIT_ENOTFOUND) + goto done; + + git_error_clear(); + do_rewrite = 1; + } + + error = do_rewrite ? + git_config__get_string_buf(out, config, "notes.rewriteref") : + GIT_ENOTFOUND; + +done: + git_config_free(config); + return error; +} + +static int rebase_copy_note( + git_rebase *rebase, + const char *notes_ref, + git_oid *from, + git_oid *to, + const git_signature *committer) +{ + git_note *note = NULL; + git_oid note_id; + git_signature *who = NULL; + int error; + + if ((error = git_note_read(¬e, rebase->repo, notes_ref, from)) < 0) { + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = 0; + } + + goto done; + } + + if (!committer) { + if((error = git_signature_default(&who, rebase->repo)) < 0) { + if (error != GIT_ENOTFOUND || + (error = git_signature_now(&who, "unknown", "unknown")) < 0) + goto done; + + git_error_clear(); + } + + committer = who; + } + + error = git_note_create(¬e_id, rebase->repo, notes_ref, + git_note_author(note), committer, to, git_note_message(note), 0); + +done: + git_note_free(note); + git_signature_free(who); + + return error; +} + +static int rebase_copy_notes( + git_rebase *rebase, + const git_signature *committer) +{ + git_str path = GIT_STR_INIT, + rewritten = GIT_STR_INIT, + notes_ref = GIT_STR_INIT; + char *pair_list, *fromstr, *tostr, *end; + git_oid from, to; + unsigned int linenum = 1; + int error = 0; + + if ((error = notes_ref_lookup(¬es_ref, rebase)) < 0) { + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = 0; + } + + goto done; + } + + if ((error = git_str_joinpath(&path, rebase->state_path, REWRITTEN_FILE)) < 0 || + (error = git_futils_readbuffer(&rewritten, path.ptr)) < 0) + goto done; + + pair_list = rewritten.ptr; + + while (*pair_list) { + fromstr = pair_list; + + if ((end = strchr(fromstr, '\n')) == NULL) + goto on_error; + + pair_list = end+1; + *end = '\0'; + + if ((end = strchr(fromstr, ' ')) == NULL) + goto on_error; + + tostr = end+1; + *end = '\0'; + + if (strlen(fromstr) != git_oid_hexsize(rebase->repo->oid_type) || + strlen(tostr) != git_oid_hexsize(rebase->repo->oid_type) || + git_oid__fromstr(&from, fromstr, rebase->repo->oid_type) < 0 || + git_oid__fromstr(&to, tostr, rebase->repo->oid_type) < 0) + goto on_error; + + if ((error = rebase_copy_note(rebase, notes_ref.ptr, &from, &to, committer)) < 0) + goto done; + + linenum++; + } + + goto done; + +on_error: + git_error_set(GIT_ERROR_REBASE, "invalid rewritten file at line %d", linenum); + error = -1; + +done: + git_str_dispose(&rewritten); + git_str_dispose(&path); + git_str_dispose(¬es_ref); + + return error; +} + +static int return_to_orig_head(git_rebase *rebase) +{ + git_reference *terminal_ref = NULL, *branch_ref = NULL, *head_ref = NULL; + git_commit *terminal_commit = NULL; + git_str branch_msg = GIT_STR_INIT, head_msg = GIT_STR_INIT; + char onto[GIT_OID_MAX_HEXSIZE + 1]; + int error = 0; + + git_oid_tostr(onto, GIT_OID_MAX_HEXSIZE + 1, &rebase->onto_id); + + if ((error = git_str_printf(&branch_msg, + "rebase finished: %s onto %s", rebase->orig_head_name, onto)) == 0 && + (error = git_str_printf(&head_msg, + "rebase finished: returning to %s", rebase->orig_head_name)) == 0 && + (error = git_repository_head(&terminal_ref, rebase->repo)) == 0 && + (error = git_reference_peel((git_object **)&terminal_commit, + terminal_ref, GIT_OBJECT_COMMIT)) == 0 && + (error = git_reference_create_matching(&branch_ref, + rebase->repo, rebase->orig_head_name, + git_commit_id(terminal_commit), 1, + &rebase->orig_head_id, branch_msg.ptr)) == 0) + error = git_reference_symbolic_create(&head_ref, + rebase->repo, GIT_HEAD_FILE, rebase->orig_head_name, 1, + head_msg.ptr); + + git_str_dispose(&head_msg); + git_str_dispose(&branch_msg); + git_commit_free(terminal_commit); + git_reference_free(head_ref); + git_reference_free(branch_ref); + git_reference_free(terminal_ref); + + return error; +} + +int git_rebase_finish( + git_rebase *rebase, + const git_signature *signature) +{ + int error = 0; + + GIT_ASSERT_ARG(rebase); + + if (rebase->inmemory) + return 0; + + if (!rebase->head_detached) + error = return_to_orig_head(rebase); + + if (error == 0 && (error = rebase_copy_notes(rebase, signature)) == 0) + error = rebase_cleanup(rebase); + + return error; +} + +const char *git_rebase_orig_head_name(git_rebase *rebase) { + GIT_ASSERT_ARG_WITH_RETVAL(rebase, NULL); + return rebase->orig_head_name; +} + +const git_oid *git_rebase_orig_head_id(git_rebase *rebase) { + GIT_ASSERT_ARG_WITH_RETVAL(rebase, NULL); + return &rebase->orig_head_id; +} + +const char *git_rebase_onto_name(git_rebase *rebase) { + GIT_ASSERT_ARG_WITH_RETVAL(rebase, NULL); + return rebase->onto_name; +} + +const git_oid *git_rebase_onto_id(git_rebase *rebase) { + return &rebase->onto_id; +} + +size_t git_rebase_operation_entrycount(git_rebase *rebase) +{ + GIT_ASSERT_ARG_WITH_RETVAL(rebase, 0); + + return git_array_size(rebase->operations); +} + +size_t git_rebase_operation_current(git_rebase *rebase) +{ + GIT_ASSERT_ARG_WITH_RETVAL(rebase, 0); + + return rebase->started ? rebase->current : GIT_REBASE_NO_OPERATION; +} + +git_rebase_operation *git_rebase_operation_byindex(git_rebase *rebase, size_t idx) +{ + GIT_ASSERT_ARG_WITH_RETVAL(rebase, NULL); + + return git_array_get(rebase->operations, idx); +} + +void git_rebase_free(git_rebase *rebase) +{ + if (rebase == NULL) + return; + + git_index_free(rebase->index); + git_commit_free(rebase->last_commit); + git__free(rebase->onto_name); + git__free(rebase->orig_head_name); + git__free(rebase->state_path); + git_str_dispose(&rebase->state_filename); + git_array_clear(rebase->operations); + git__free((char *)rebase->options.rewrite_notes_ref); + git__free(rebase); +} diff --git a/src/libgit2/refdb.c b/src/libgit2/refdb.c new file mode 100644 index 0000000..ed33de9 --- /dev/null +++ b/src/libgit2/refdb.c @@ -0,0 +1,424 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "refdb.h" + +#include "git2/object.h" +#include "git2/refs.h" +#include "git2/refdb.h" +#include "git2/sys/refdb_backend.h" + +#include "hash.h" +#include "refs.h" +#include "reflog.h" +#include "posix.h" + +#define DEFAULT_NESTING_LEVEL 5 +#define MAX_NESTING_LEVEL 10 + +int git_refdb_new(git_refdb **out, git_repository *repo) +{ + git_refdb *db; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + db = git__calloc(1, sizeof(*db)); + GIT_ERROR_CHECK_ALLOC(db); + + db->repo = repo; + + *out = db; + GIT_REFCOUNT_INC(db); + return 0; +} + +int git_refdb_open(git_refdb **out, git_repository *repo) +{ + git_refdb *db; + git_refdb_backend *dir; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + *out = NULL; + + if (git_refdb_new(&db, repo) < 0) + return -1; + + /* Add the default (filesystem) backend */ + if (git_refdb_backend_fs(&dir, repo) < 0) { + git_refdb_free(db); + return -1; + } + + db->repo = repo; + db->backend = dir; + + *out = db; + return 0; +} + +static void refdb_free_backend(git_refdb *db) +{ + if (db->backend) + db->backend->free(db->backend); +} + +int git_refdb_set_backend(git_refdb *db, git_refdb_backend *backend) +{ + GIT_ERROR_CHECK_VERSION(backend, GIT_REFDB_BACKEND_VERSION, "git_refdb_backend"); + + if (!backend->exists || !backend->lookup || !backend->iterator || + !backend->write || !backend->rename || !backend->del || + !backend->has_log || !backend->ensure_log || !backend->free || + !backend->reflog_read || !backend->reflog_write || + !backend->reflog_rename || !backend->reflog_delete || + (backend->lock && !backend->unlock)) { + git_error_set(GIT_ERROR_REFERENCE, "incomplete refdb backend implementation"); + return GIT_EINVALID; + } + + refdb_free_backend(db); + db->backend = backend; + + return 0; +} + +int git_refdb_compress(git_refdb *db) +{ + GIT_ASSERT_ARG(db); + + if (db->backend->compress) + return db->backend->compress(db->backend); + + return 0; +} + +void git_refdb__free(git_refdb *db) +{ + refdb_free_backend(db); + git__memzero(db, sizeof(*db)); + git__free(db); +} + +void git_refdb_free(git_refdb *db) +{ + if (db == NULL) + return; + + GIT_REFCOUNT_DEC(db, git_refdb__free); +} + +int git_refdb_exists(int *exists, git_refdb *refdb, const char *ref_name) +{ + GIT_ASSERT_ARG(exists); + GIT_ASSERT_ARG(refdb); + GIT_ASSERT_ARG(refdb->backend); + + return refdb->backend->exists(exists, refdb->backend, ref_name); +} + +int git_refdb_lookup(git_reference **out, git_refdb *db, const char *ref_name) +{ + git_reference *ref; + int error; + + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(db->backend); + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(ref_name); + + error = db->backend->lookup(&ref, db->backend, ref_name); + if (error < 0) + return error; + + GIT_REFCOUNT_INC(db); + ref->db = db; + + *out = ref; + return 0; +} + +int git_refdb_resolve( + git_reference **out, + git_refdb *db, + const char *ref_name, + int max_nesting) +{ + git_reference *ref = NULL; + int error = 0, nesting; + + *out = NULL; + + if (max_nesting > MAX_NESTING_LEVEL) + max_nesting = MAX_NESTING_LEVEL; + else if (max_nesting < 0) + max_nesting = DEFAULT_NESTING_LEVEL; + + if ((error = git_refdb_lookup(&ref, db, ref_name)) < 0) + goto out; + + for (nesting = 0; nesting < max_nesting; nesting++) { + git_reference *resolved; + + if (ref->type == GIT_REFERENCE_DIRECT) + break; + + if ((error = git_refdb_lookup(&resolved, db, git_reference_symbolic_target(ref))) < 0) { + /* If we found a symbolic reference with a nonexistent target, return it. */ + if (error == GIT_ENOTFOUND) { + error = 0; + *out = ref; + ref = NULL; + } + goto out; + } + + git_reference_free(ref); + ref = resolved; + } + + if (ref->type != GIT_REFERENCE_DIRECT && max_nesting != 0) { + git_error_set(GIT_ERROR_REFERENCE, + "cannot resolve reference (>%u levels deep)", max_nesting); + error = -1; + goto out; + } + + *out = ref; + ref = NULL; +out: + git_reference_free(ref); + return error; +} + +int git_refdb_iterator(git_reference_iterator **out, git_refdb *db, const char *glob) +{ + int error; + + if (!db->backend || !db->backend->iterator) { + git_error_set(GIT_ERROR_REFERENCE, "this backend doesn't support iterators"); + return -1; + } + + if ((error = db->backend->iterator(out, db->backend, glob)) < 0) + return error; + + GIT_REFCOUNT_INC(db); + (*out)->db = db; + + return 0; +} + +int git_refdb_iterator_next(git_reference **out, git_reference_iterator *iter) +{ + int error; + + if ((error = iter->next(out, iter)) < 0) + return error; + + GIT_REFCOUNT_INC(iter->db); + (*out)->db = iter->db; + + return 0; +} + +int git_refdb_iterator_next_name(const char **out, git_reference_iterator *iter) +{ + return iter->next_name(out, iter); +} + +void git_refdb_iterator_free(git_reference_iterator *iter) +{ + GIT_REFCOUNT_DEC(iter->db, git_refdb__free); + iter->free(iter); +} + +int git_refdb_write(git_refdb *db, git_reference *ref, int force, const git_signature *who, const char *message, const git_oid *old_id, const char *old_target) +{ + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(db->backend); + + GIT_REFCOUNT_INC(db); + ref->db = db; + + return db->backend->write(db->backend, ref, force, who, message, old_id, old_target); +} + +int git_refdb_rename( + git_reference **out, + git_refdb *db, + const char *old_name, + const char *new_name, + int force, + const git_signature *who, + const char *message) +{ + int error; + + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(db->backend); + + error = db->backend->rename(out, db->backend, old_name, new_name, force, who, message); + if (error < 0) + return error; + + if (out) { + GIT_REFCOUNT_INC(db); + (*out)->db = db; + } + + return 0; +} + +int git_refdb_delete(struct git_refdb *db, const char *ref_name, const git_oid *old_id, const char *old_target) +{ + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(db->backend); + + return db->backend->del(db->backend, ref_name, old_id, old_target); +} + +int git_refdb_reflog_read(git_reflog **out, git_refdb *db, const char *name) +{ + int error; + + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(db->backend); + + if ((error = db->backend->reflog_read(out, db->backend, name)) < 0) + return error; + + GIT_REFCOUNT_INC(db); + (*out)->db = db; + + return 0; +} + +int git_refdb_should_write_reflog(int *out, git_refdb *db, const git_reference *ref) +{ + int error, logall; + + error = git_repository__configmap_lookup(&logall, db->repo, GIT_CONFIGMAP_LOGALLREFUPDATES); + if (error < 0) + return error; + + /* Defaults to the opposite of the repo being bare */ + if (logall == GIT_LOGALLREFUPDATES_UNSET) + logall = !git_repository_is_bare(db->repo); + + *out = 0; + switch (logall) { + case GIT_LOGALLREFUPDATES_FALSE: + *out = 0; + break; + + case GIT_LOGALLREFUPDATES_TRUE: + /* Only write if it already has a log, + * or if it's under heads/, remotes/ or notes/ + */ + *out = git_refdb_has_log(db, ref->name) || + !git__prefixcmp(ref->name, GIT_REFS_HEADS_DIR) || + !git__strcmp(ref->name, GIT_HEAD_FILE) || + !git__prefixcmp(ref->name, GIT_REFS_REMOTES_DIR) || + !git__prefixcmp(ref->name, GIT_REFS_NOTES_DIR); + break; + + case GIT_LOGALLREFUPDATES_ALWAYS: + *out = 1; + break; + } + + return 0; +} + +int git_refdb_should_write_head_reflog(int *out, git_refdb *db, const git_reference *ref) +{ + git_reference *head = NULL, *resolved = NULL; + const char *name; + int error; + + *out = 0; + + if (ref->type == GIT_REFERENCE_SYMBOLIC) { + error = 0; + goto out; + } + + if ((error = git_refdb_lookup(&head, db, GIT_HEAD_FILE)) < 0) + goto out; + + if (git_reference_type(head) == GIT_REFERENCE_DIRECT) + goto out; + + /* Go down the symref chain until we find the branch */ + if ((error = git_refdb_resolve(&resolved, db, git_reference_symbolic_target(head), -1)) < 0) { + if (error != GIT_ENOTFOUND) + goto out; + error = 0; + name = git_reference_symbolic_target(head); + } else if (git_reference_type(resolved) == GIT_REFERENCE_SYMBOLIC) { + name = git_reference_symbolic_target(resolved); + } else { + name = git_reference_name(resolved); + } + + if (strcmp(name, ref->name)) + goto out; + + *out = 1; + +out: + git_reference_free(resolved); + git_reference_free(head); + return error; +} + +int git_refdb_has_log(git_refdb *db, const char *refname) +{ + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(refname); + + return db->backend->has_log(db->backend, refname); +} + +int git_refdb_ensure_log(git_refdb *db, const char *refname) +{ + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(refname); + + return db->backend->ensure_log(db->backend, refname); +} + +int git_refdb_init_backend(git_refdb_backend *backend, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + backend, version, git_refdb_backend, GIT_REFDB_BACKEND_INIT); + return 0; +} + +int git_refdb_lock(void **payload, git_refdb *db, const char *refname) +{ + GIT_ASSERT_ARG(payload); + GIT_ASSERT_ARG(db); + GIT_ASSERT_ARG(refname); + + if (!db->backend->lock) { + git_error_set(GIT_ERROR_REFERENCE, "backend does not support locking"); + return -1; + } + + return db->backend->lock(payload, db->backend, refname); +} + +int git_refdb_unlock(git_refdb *db, void *payload, int success, int update_reflog, const git_reference *ref, const git_signature *sig, const char *message) +{ + GIT_ASSERT_ARG(db); + + return db->backend->unlock(db->backend, payload, success, update_reflog, ref, sig, message); +} diff --git a/src/libgit2/refdb.h b/src/libgit2/refdb.h new file mode 100644 index 0000000..84e19b1 --- /dev/null +++ b/src/libgit2/refdb.h @@ -0,0 +1,128 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_refdb_h__ +#define INCLUDE_refdb_h__ + +#include "common.h" + +#include "git2/refdb.h" +#include "repository.h" + +struct git_refdb { + git_refcount rc; + git_repository *repo; + git_refdb_backend *backend; +}; + +void git_refdb__free(git_refdb *db); + +int git_refdb_exists( + int *exists, + git_refdb *refdb, + const char *ref_name); + +int git_refdb_lookup( + git_reference **out, + git_refdb *refdb, + const char *ref_name); + +/** + * Resolve the reference by following symbolic references. + * + * Given a reference name, this function will follow any symbolic references up + * to `max_nesting` deep and return the resolved direct reference. If any of + * the intermediate symbolic references points to a non-existing reference, + * then that symbolic reference is returned instead with an error code of `0`. + * If the given reference is a direct reference already, it is returned + * directly. + * + * If `max_nesting` is `0`, the reference will not be resolved. If it's + * negative, it will be set to the default resolve depth which is `5`. + * + * @param out Pointer to store the result in. + * @param db The refdb to use for resolving the reference. + * @param ref_name The reference name to lookup and resolve. + * @param max_nesting The maximum nesting depth. + * @return `0` on success, a negative error code otherwise. + */ +int git_refdb_resolve( + git_reference **out, + git_refdb *db, + const char *ref_name, + int max_nesting); + +int git_refdb_rename( + git_reference **out, + git_refdb *db, + const char *old_name, + const char *new_name, + int force, + const git_signature *who, + const char *message); + +int git_refdb_iterator(git_reference_iterator **out, git_refdb *db, const char *glob); +int git_refdb_iterator_next(git_reference **out, git_reference_iterator *iter); +int git_refdb_iterator_next_name(const char **out, git_reference_iterator *iter); +void git_refdb_iterator_free(git_reference_iterator *iter); + +int git_refdb_write(git_refdb *refdb, git_reference *ref, int force, const git_signature *who, const char *message, const git_oid *old_id, const char *old_target); +int git_refdb_delete(git_refdb *refdb, const char *ref_name, const git_oid *old_id, const char *old_target); + +int git_refdb_reflog_read(git_reflog **out, git_refdb *db, const char *name); +int git_refdb_reflog_write(git_reflog *reflog); + +/** + * Determine whether a reflog entry should be created for the given reference. + * + * Whether or not writing to a reference should create a reflog entry is + * dependent on a number of things. Most importantly, there's the + * "core.logAllRefUpdates" setting that controls in which situations a + * reference should get a corresponding reflog entry. The following values for + * it are understood: + * + * - "false": Do not log reference updates. + * + * - "true": Log normal reference updates. This will write entries for + * references in "refs/heads", "refs/remotes", "refs/notes" and + * "HEAD" or if the reference already has a log entry. + * + * - "always": Always create a reflog entry. + * + * If unset, the value will default to "true" for non-bare repositories and + * "false" for bare ones. + * + * @param out pointer to which the result will be written, `1` means a reflog + * entry should be written, `0` means none should be written. + * @param db The refdb to decide this for. + * @param ref The reference one wants to check. + * @return `0` on success, a negative error code otherwise. + */ +int git_refdb_should_write_reflog(int *out, git_refdb *db, const git_reference *ref); + +/** + * Determine whether a reflog entry should be created for HEAD if creating one + * for the given reference + * + * In case the given reference is being pointed to by HEAD, then creating a + * reflog entry for this reference also requires us to create a corresponding + * reflog entry for HEAD. This function can be used to determine that scenario. + * + * @param out pointer to which the result will be written, `1` means a reflog + * entry should be written, `0` means none should be written. + * @param db The refdb to decide this for. + * @param ref The reference one wants to check. + * @return `0` on success, a negative error code otherwise. + */ +int git_refdb_should_write_head_reflog(int *out, git_refdb *db, const git_reference *ref); + +int git_refdb_has_log(git_refdb *db, const char *refname); +int git_refdb_ensure_log(git_refdb *refdb, const char *refname); + +int git_refdb_lock(void **payload, git_refdb *db, const char *refname); +int git_refdb_unlock(git_refdb *db, void *payload, int success, int update_reflog, const git_reference *ref, const git_signature *sig, const char *message); + +#endif diff --git a/src/libgit2/refdb_fs.c b/src/libgit2/refdb_fs.c new file mode 100644 index 0000000..e34a714 --- /dev/null +++ b/src/libgit2/refdb_fs.c @@ -0,0 +1,2508 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "refs.h" +#include "hash.h" +#include "repository.h" +#include "futils.h" +#include "filebuf.h" +#include "pack.h" +#include "parse.h" +#include "reflog.h" +#include "refdb.h" +#include "iterator.h" +#include "sortedcache.h" +#include "signature.h" +#include "wildmatch.h" +#include "path.h" + +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_NESTING_LEVEL 5 +#define MAX_NESTING_LEVEL 10 + +enum { + PACKREF_HAS_PEEL = 1, + PACKREF_WAS_LOOSE = 2, + PACKREF_CANNOT_PEEL = 4, + PACKREF_SHADOWED = 8 +}; + +enum { + PEELING_NONE = 0, + PEELING_STANDARD, + PEELING_FULL +}; + +struct packref { + git_oid oid; + git_oid peel; + char flags; + char name[GIT_FLEX_ARRAY]; +}; + +typedef struct refdb_fs_backend { + git_refdb_backend parent; + + git_repository *repo; + /* path to git directory */ + char *gitpath; + /* path to common objects' directory */ + char *commonpath; + + git_oid_t oid_type; + + int fsync : 1, + sorted : 1; + int peeling_mode; + git_iterator_flag_t iterator_flags; + uint32_t direach_flags; + git_sortedcache *refcache; + git_map packed_refs_map; + git_mutex prlock; /* protect packed_refs_map */ + git_futils_filestamp packed_refs_stamp; +} refdb_fs_backend; + +static int refdb_reflog_fs__delete(git_refdb_backend *_backend, const char *name); +static char *packed_set_peeling_mode(char *data, size_t data_sz, refdb_fs_backend *backend); + +GIT_INLINE(int) loose_path( + git_str *out, + const char *base, + const char *refname) +{ + if (git_str_joinpath(out, base, refname) < 0) + return -1; + + return git_fs_path_validate_str_length_with_suffix(out, + CONST_STRLEN(".lock")); +} + +GIT_INLINE(int) reflog_path( + git_str *out, + git_repository *repo, + const char *refname) +{ + const char *base; + int error; + + base = (strcmp(refname, GIT_HEAD_FILE) == 0) ? repo->gitdir : + repo->commondir; + + if ((error = git_str_joinpath(out, base, GIT_REFLOG_DIR)) < 0) + return error; + + return loose_path(out, out->ptr, refname); +} + +static int packref_cmp(const void *a_, const void *b_) +{ + const struct packref *a = a_, *b = b_; + return strcmp(a->name, b->name); +} + +static int packed_reload(refdb_fs_backend *backend) +{ + int error; + git_str packedrefs = GIT_STR_INIT; + size_t oid_hexsize = git_oid_hexsize(backend->oid_type); + char *scan, *eof, *eol; + + if (!backend->gitpath) + return 0; + + error = git_sortedcache_lockandload(backend->refcache, &packedrefs); + + /* + * If we can't find the packed-refs, clear table and return. + * Any other error just gets passed through. + * If no error, and file wasn't changed, just return. + * Anything else means we need to refresh the packed refs. + */ + if (error <= 0) { + if (error == GIT_ENOTFOUND) { + GIT_UNUSED(git_sortedcache_clear(backend->refcache, true)); + git_error_clear(); + error = 0; + } + return error; + } + + /* At this point, refresh the packed refs from the loaded buffer. */ + + GIT_UNUSED(git_sortedcache_clear(backend->refcache, false)); + + scan = packedrefs.ptr; + eof = scan + packedrefs.size; + + scan = packed_set_peeling_mode(scan, packedrefs.size, backend); + if (!scan) + goto parse_failed; + + while (scan < eof && *scan == '#') { + if (!(eol = strchr(scan, '\n'))) + goto parse_failed; + scan = eol + 1; + } + + while (scan < eof) { + struct packref *ref; + git_oid oid; + + /* parse " \n" */ + + if (git_oid__fromstr(&oid, scan, backend->oid_type) < 0) + goto parse_failed; + scan += oid_hexsize; + + if (*scan++ != ' ') + goto parse_failed; + if (!(eol = strchr(scan, '\n'))) + goto parse_failed; + *eol = '\0'; + if (eol[-1] == '\r') + eol[-1] = '\0'; + + if (git_sortedcache_upsert((void **)&ref, backend->refcache, scan) < 0) + goto parse_failed; + scan = eol + 1; + + git_oid_cpy(&ref->oid, &oid); + + /* look for optional "^\n" */ + + if (*scan == '^') { + if (git_oid__fromstr(&oid, scan + 1, backend->oid_type) < 0) + goto parse_failed; + scan += oid_hexsize + 1; + + if (scan < eof) { + if (!(eol = strchr(scan, '\n'))) + goto parse_failed; + scan = eol + 1; + } + + git_oid_cpy(&ref->peel, &oid); + ref->flags |= PACKREF_HAS_PEEL; + } + else if (backend->peeling_mode == PEELING_FULL || + (backend->peeling_mode == PEELING_STANDARD && + git__prefixcmp(ref->name, GIT_REFS_TAGS_DIR) == 0)) + ref->flags |= PACKREF_CANNOT_PEEL; + } + + git_sortedcache_wunlock(backend->refcache); + git_str_dispose(&packedrefs); + + return 0; + +parse_failed: + git_error_set(GIT_ERROR_REFERENCE, "corrupted packed references file"); + + GIT_UNUSED(git_sortedcache_clear(backend->refcache, false)); + git_sortedcache_wunlock(backend->refcache); + git_str_dispose(&packedrefs); + + return -1; +} + +static int loose_parse_oid( + git_oid *oid, + const char *filename, + git_str *file_content, + git_oid_t oid_type) +{ + const char *str = git_str_cstr(file_content); + size_t oid_hexsize = git_oid_hexsize(oid_type); + + if (git_str_len(file_content) < oid_hexsize) + goto corrupted; + + /* we need to get 40 OID characters from the file */ + if (git_oid__fromstr(oid, str, oid_type) < 0) + goto corrupted; + + /* If the file is longer than 40 chars, the 41st must be a space */ + str += oid_hexsize; + if (*str == '\0' || git__isspace(*str)) + return 0; + +corrupted: + git_error_set(GIT_ERROR_REFERENCE, "corrupted loose reference file: %s", filename); + return -1; +} + +static int loose_readbuffer(git_str *buf, const char *base, const char *path) +{ + int error; + + if ((error = loose_path(buf, base, path)) < 0 || + (error = git_futils_readbuffer(buf, buf->ptr)) < 0) + git_str_dispose(buf); + + return error; +} + +static int loose_lookup_to_packfile(refdb_fs_backend *backend, const char *name) +{ + int error = 0; + git_str ref_file = GIT_STR_INIT; + struct packref *ref = NULL; + git_oid oid; + + /* if we fail to load the loose reference, assume someone changed + * the filesystem under us and skip it... + */ + if (loose_readbuffer(&ref_file, backend->gitpath, name) < 0) { + git_error_clear(); + goto done; + } + + /* skip symbolic refs */ + if (!git__prefixcmp(git_str_cstr(&ref_file), GIT_SYMREF)) + goto done; + + /* parse OID from file */ + if ((error = loose_parse_oid(&oid, name, &ref_file, backend->oid_type)) < 0) + goto done; + + if ((error = git_sortedcache_wlock(backend->refcache)) < 0) + goto done; + + if (!(error = git_sortedcache_upsert( + (void **)&ref, backend->refcache, name))) { + + git_oid_cpy(&ref->oid, &oid); + ref->flags = PACKREF_WAS_LOOSE; + } + + git_sortedcache_wunlock(backend->refcache); + +done: + git_str_dispose(&ref_file); + return error; +} + +static int _dirent_loose_load(void *payload, git_str *full_path) +{ + refdb_fs_backend *backend = payload; + const char *file_path; + + if (git__suffixcmp(full_path->ptr, ".lock") == 0) + return 0; + + if (git_fs_path_isdir(full_path->ptr)) { + int error = git_fs_path_direach( + full_path, backend->direach_flags, _dirent_loose_load, backend); + /* Race with the filesystem, ignore it */ + if (error == GIT_ENOTFOUND) { + git_error_clear(); + return 0; + } + + return error; + } + + file_path = full_path->ptr + strlen(backend->gitpath); + + return loose_lookup_to_packfile(backend, file_path); +} + +/* + * Load all the loose references from the repository + * into the in-memory Packfile, and build a vector with + * all the references so it can be written back to + * disk. + */ +static int packed_loadloose(refdb_fs_backend *backend) +{ + int error; + git_str refs_path = GIT_STR_INIT; + + if (git_str_joinpath(&refs_path, backend->gitpath, GIT_REFS_DIR) < 0) + return -1; + + /* + * Load all the loose files from disk into the Packfile table. + * This will overwrite any old packed entries with their + * updated loose versions + */ + error = git_fs_path_direach( + &refs_path, backend->direach_flags, _dirent_loose_load, backend); + + git_str_dispose(&refs_path); + + return error; +} + +static int refdb_fs_backend__exists( + int *exists, + git_refdb_backend *_backend, + const char *ref_name) +{ + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + git_str ref_path = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(backend); + + *exists = 0; + + if ((error = loose_path(&ref_path, backend->gitpath, ref_name)) < 0) + goto out; + + if (git_fs_path_isfile(ref_path.ptr)) { + *exists = 1; + goto out; + } + + if ((error = packed_reload(backend)) < 0) + goto out; + + if (git_sortedcache_lookup(backend->refcache, ref_name) != NULL) { + *exists = 1; + goto out; + } + +out: + git_str_dispose(&ref_path); + return error; +} + +static const char *loose_parse_symbolic(git_str *file_content) +{ + const unsigned int header_len = (unsigned int)strlen(GIT_SYMREF); + const char *refname_start; + + refname_start = (const char *)file_content->ptr; + + if (git_str_len(file_content) < header_len + 1) { + git_error_set(GIT_ERROR_REFERENCE, "corrupted loose reference file"); + return NULL; + } + + /* + * Assume we have already checked for the header + * before calling this function + */ + refname_start += header_len; + + return refname_start; +} + +/* + * Returns whether a reference is stored per worktree or not. + * Per-worktree references are: + * + * - all pseudorefs, e.g. HEAD and MERGE_HEAD + * - all references stored inside of "refs/bisect/" + */ +static bool is_per_worktree_ref(const char *ref_name) +{ + return git__prefixcmp(ref_name, "refs/") != 0 || + git__prefixcmp(ref_name, "refs/bisect/") == 0; +} + +static int loose_lookup( + git_reference **out, + refdb_fs_backend *backend, + const char *ref_name) +{ + git_str ref_file = GIT_STR_INIT; + int error = 0; + const char *ref_dir; + + if (out) + *out = NULL; + + if (is_per_worktree_ref(ref_name)) + ref_dir = backend->gitpath; + else + ref_dir = backend->commonpath; + + if ((error = loose_readbuffer(&ref_file, ref_dir, ref_name)) < 0) + /* cannot read loose ref file - gah */; + else if (git__prefixcmp(git_str_cstr(&ref_file), GIT_SYMREF) == 0) { + const char *target; + + git_str_rtrim(&ref_file); + + if (!(target = loose_parse_symbolic(&ref_file))) + error = -1; + else if (out != NULL) + *out = git_reference__alloc_symbolic(ref_name, target); + } else { + git_oid oid; + + if (!(error = loose_parse_oid(&oid, ref_name, &ref_file, backend->oid_type)) && + out != NULL) + *out = git_reference__alloc(ref_name, &oid, NULL); + } + + git_str_dispose(&ref_file); + return error; +} + +static int ref_error_notfound(const char *name) +{ + git_error_set(GIT_ERROR_REFERENCE, "reference '%s' not found", name); + return GIT_ENOTFOUND; +} + +static char *packed_set_peeling_mode( + char *data, + size_t data_sz, + refdb_fs_backend *backend) +{ + static const char *traits_header = "# pack-refs with:"; + char *eol; + backend->peeling_mode = PEELING_NONE; + + if (git__prefixncmp(data, data_sz, traits_header) == 0) { + size_t hdr_sz = strlen(traits_header); + const char *sorted = " sorted "; + const char *peeled = " peeled "; + const char *fully_peeled = " fully-peeled "; + data += hdr_sz; + data_sz -= hdr_sz; + + eol = memchr(data, '\n', data_sz); + + if (!eol) + return NULL; + + if (git__memmem(data, eol - data, fully_peeled, strlen(fully_peeled))) + backend->peeling_mode = PEELING_FULL; + else if (git__memmem(data, eol - data, peeled, strlen(peeled))) + backend->peeling_mode = PEELING_STANDARD; + + backend->sorted = NULL != git__memmem(data, eol - data, sorted, strlen(sorted)); + + return eol + 1; + } + return data; +} + +static void packed_map_free(refdb_fs_backend *backend) +{ + if (backend->packed_refs_map.data) { +#ifdef GIT_WIN32 + git__free(backend->packed_refs_map.data); +#else + git_futils_mmap_free(&backend->packed_refs_map); +#endif + backend->packed_refs_map.data = NULL; + backend->packed_refs_map.len = 0; + git_futils_filestamp_set(&backend->packed_refs_stamp, NULL); + } +} + +static int packed_map_check(refdb_fs_backend *backend) +{ + int error = 0; + git_file fd = -1; + struct stat st; + + if ((error = git_mutex_lock(&backend->prlock)) < 0) + return error; + + if (backend->packed_refs_map.data && + !git_futils_filestamp_check( + &backend->packed_refs_stamp, backend->refcache->path)) { + git_mutex_unlock(&backend->prlock); + return error; + } + packed_map_free(backend); + + fd = git_futils_open_ro(backend->refcache->path); + if (fd < 0) { + git_mutex_unlock(&backend->prlock); + if (fd == GIT_ENOTFOUND) { + git_error_clear(); + return 0; + } + return fd; + } + + if (p_fstat(fd, &st) < 0) { + p_close(fd); + git_mutex_unlock(&backend->prlock); + git_error_set(GIT_ERROR_OS, "unable to stat packed-refs '%s'", backend->refcache->path); + return -1; + } + + if (st.st_size == 0) { + p_close(fd); + git_mutex_unlock(&backend->prlock); + return 0; + } + + git_futils_filestamp_set_from_stat(&backend->packed_refs_stamp, &st); + +#ifdef GIT_WIN32 + /* on windows, we copy the entire file into memory rather than using + * mmap() because using mmap() on windows also locks the file and this + * map is long-lived. */ + backend->packed_refs_map.len = (size_t)st.st_size; + backend->packed_refs_map.data = + git__malloc(backend->packed_refs_map.len); + GIT_ERROR_CHECK_ALLOC(backend->packed_refs_map.data); + { + ssize_t bytesread = + p_read(fd, backend->packed_refs_map.data, + backend->packed_refs_map.len); + error = (bytesread == (ssize_t)backend->packed_refs_map.len) ? 0 : -1; + } +#else + error = git_futils_mmap_ro(&backend->packed_refs_map, fd, 0, (size_t)st.st_size); +#endif + p_close(fd); + if (error < 0) { + git_mutex_unlock(&backend->prlock); + return error; + } + + packed_set_peeling_mode( + backend->packed_refs_map.data, backend->packed_refs_map.len, + backend); + + git_mutex_unlock(&backend->prlock); + return error; +} + +/* + * Find beginning of packed-ref record pointed to by p. + * buf - a lower-bound pointer to some memory buffer + * p - an upper-bound pointer to the same memory buffer + */ +static const char *start_of_record(const char *buf, const char *p) +{ + const char *nl = p; + while (true) { + nl = git__memrchr(buf, '\n', nl - buf); + if (!nl) + return buf; + + if (nl[1] == '^' && nl > buf) + --nl; + else + break; + }; + return nl + 1; +} + +/* + * Find end of packed-ref record pointed to by p. + * end - an upper-bound pointer to some memory buffer + * p - a lower-bound pointer to the same memory buffer + */ +static const char *end_of_record(const char *p, const char *end) +{ + while (1) { + size_t sz = end - p; + p = memchr(p, '\n', sz); + if (!p) + return end; + ++p; + if (p < end && p[0] == '^') + ++p; + else + break; + } + return p; +} + +static int cmp_record_to_refname( + const char *rec, + size_t data_end, + const char *ref_name, + git_oid_t oid_type) +{ + const size_t ref_len = strlen(ref_name); + int cmp_val; + const char *end; + size_t oid_hexsize = git_oid_hexsize(oid_type); + + rec += oid_hexsize + 1; /* + space */ + + /* an incomplete (corrupt) record is treated as less than ref_name */ + if (data_end < oid_hexsize + 3) + return -1; + + data_end -= oid_hexsize + 1; + + end = memchr(rec, '\n', data_end); + if (end) + data_end = end - rec; + + cmp_val = memcmp(rec, ref_name, min(ref_len, data_end)); + + if (cmp_val == 0 && data_end != ref_len) + return (data_end > ref_len) ? 1 : -1; + return cmp_val; +} + +static int packed_unsorted_lookup( + git_reference **out, + refdb_fs_backend *backend, + const char *ref_name) +{ + int error = 0; + struct packref *entry; + + if ((error = packed_reload(backend)) < 0) + return error; + + if (git_sortedcache_rlock(backend->refcache) < 0) + return -1; + + entry = git_sortedcache_lookup(backend->refcache, ref_name); + if (!entry) { + error = ref_error_notfound(ref_name); + } else { + *out = git_reference__alloc(ref_name, &entry->oid, &entry->peel); + if (!*out) + error = -1; + } + + git_sortedcache_runlock(backend->refcache); + + return error; +} + +static int packed_lookup( + git_reference **out, + refdb_fs_backend *backend, + const char *ref_name) +{ + int error = 0; + const char *left, *right, *data_end; + size_t oid_hexsize = git_oid_hexsize(backend->oid_type); + + if ((error = packed_map_check(backend)) < 0) + return error; + + if (!backend->sorted) + return packed_unsorted_lookup(out, backend, ref_name); + + left = backend->packed_refs_map.data; + right = data_end = (const char *) backend->packed_refs_map.data + + backend->packed_refs_map.len; + + while (left < right && *left == '#') { + if (!(left = memchr(left, '\n', data_end - left))) + goto parse_failed; + left++; + } + + while (left < right) { + const char *mid, *rec; + int compare; + + mid = left + (right - left) / 2; + rec = start_of_record(left, mid); + compare = cmp_record_to_refname(rec, data_end - rec, ref_name, backend->oid_type); + + if (compare < 0) { + left = end_of_record(mid, right); + } else if (compare > 0) { + right = rec; + } else { + const char *eol; + git_oid oid, peel, *peel_ptr = NULL; + + if (data_end - rec < (long)oid_hexsize || + git_oid__fromstr(&oid, rec, backend->oid_type) < 0) { + goto parse_failed; + } + rec += oid_hexsize + 1; + if (!(eol = memchr(rec, '\n', data_end - rec))) { + goto parse_failed; + } + + /* look for optional "^\n" */ + + if (eol + 1 < data_end) { + rec = eol + 1; + + if (*rec == '^') { + rec++; + if (data_end - rec < (long)oid_hexsize || + git_oid__fromstr(&peel, rec, backend->oid_type) < 0) { + goto parse_failed; + } + peel_ptr = &peel; + } + } + + *out = git_reference__alloc(ref_name, &oid, peel_ptr); + if (!*out) { + return -1; + } + + return 0; + } + } + return ref_error_notfound(ref_name); + +parse_failed: + git_error_set(GIT_ERROR_REFERENCE, "corrupted packed references file"); + return -1; +} + +static int refdb_fs_backend__lookup( + git_reference **out, + git_refdb_backend *_backend, + const char *ref_name) +{ + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + int error; + + GIT_ASSERT_ARG(backend); + + if (!(error = loose_lookup(out, backend, ref_name))) + return 0; + + /* only try to lookup this reference on the packfile if it + * wasn't found on the loose refs; not if there was a critical error */ + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = packed_lookup(out, backend, ref_name); + } + return error; +} + +typedef struct { + git_reference_iterator parent; + + char *glob; + + git_pool pool; + git_vector loose; + + git_sortedcache *cache; + size_t loose_pos; + size_t packed_pos; +} refdb_fs_iter; + +static void refdb_fs_backend__iterator_free(git_reference_iterator *_iter) +{ + refdb_fs_iter *iter = GIT_CONTAINER_OF(_iter, refdb_fs_iter, parent); + + git_vector_free(&iter->loose); + git_pool_clear(&iter->pool); + git_sortedcache_free(iter->cache); + git__free(iter); +} + +static int iter_load_loose_paths( + refdb_fs_backend *backend, + refdb_fs_iter *iter) +{ + int error = 0; + git_str path = GIT_STR_INIT; + git_iterator *fsit = NULL; + git_iterator_options fsit_opts = GIT_ITERATOR_OPTIONS_INIT; + const git_index_entry *entry = NULL; + const char *ref_prefix = GIT_REFS_DIR; + size_t ref_prefix_len = strlen(ref_prefix); + + if (!backend->commonpath) /* do nothing if no commonpath for loose refs */ + return 0; + + fsit_opts.flags = backend->iterator_flags; + fsit_opts.oid_type = backend->oid_type; + + if (iter->glob) { + const char *last_sep = NULL; + const char *pos; + for (pos = iter->glob; *pos; ++pos) { + switch (*pos) { + case '?': + case '*': + case '[': + case '\\': + break; + case '/': + last_sep = pos; + /* FALLTHROUGH */ + default: + continue; + } + break; + } + if (last_sep) { + ref_prefix = iter->glob; + ref_prefix_len = (last_sep - ref_prefix) + 1; + } + } + + if ((error = git_str_puts(&path, backend->commonpath)) < 0 || + (error = git_str_put(&path, ref_prefix, ref_prefix_len)) < 0) { + git_str_dispose(&path); + return error; + } + + if ((error = git_iterator_for_filesystem(&fsit, path.ptr, &fsit_opts)) < 0) { + git_str_dispose(&path); + return (iter->glob && error == GIT_ENOTFOUND)? 0 : error; + } + + error = git_str_sets(&path, ref_prefix); + + while (!error && !git_iterator_advance(&entry, fsit)) { + const char *ref_name; + char *ref_dup; + + git_str_truncate(&path, ref_prefix_len); + git_str_puts(&path, entry->path); + ref_name = git_str_cstr(&path); + + if (git__suffixcmp(ref_name, ".lock") == 0 || + (iter->glob && wildmatch(iter->glob, ref_name, 0) != 0)) + continue; + + ref_dup = git_pool_strdup(&iter->pool, ref_name); + if (!ref_dup) + error = -1; + else + error = git_vector_insert(&iter->loose, ref_dup); + } + + git_iterator_free(fsit); + git_str_dispose(&path); + + return error; +} + +static int refdb_fs_backend__iterator_next( + git_reference **out, git_reference_iterator *_iter) +{ + int error = GIT_ITEROVER; + refdb_fs_iter *iter = GIT_CONTAINER_OF(_iter, refdb_fs_iter, parent); + refdb_fs_backend *backend = GIT_CONTAINER_OF(iter->parent.db->backend, refdb_fs_backend, parent); + struct packref *ref; + + while (iter->loose_pos < iter->loose.length) { + const char *path = git_vector_get(&iter->loose, iter->loose_pos++); + + if (loose_lookup(out, backend, path) == 0) { + ref = git_sortedcache_lookup(iter->cache, path); + if (ref) + ref->flags |= PACKREF_SHADOWED; + + return 0; + } + + git_error_clear(); + } + + error = GIT_ITEROVER; + while (iter->packed_pos < git_sortedcache_entrycount(iter->cache)) { + ref = git_sortedcache_entry(iter->cache, iter->packed_pos++); + if (!ref) /* stop now if another thread deleted refs and we past end */ + break; + + if (ref->flags & PACKREF_SHADOWED) + continue; + if (iter->glob && wildmatch(iter->glob, ref->name, 0) != 0) + continue; + + *out = git_reference__alloc(ref->name, &ref->oid, &ref->peel); + error = (*out != NULL) ? 0 : -1; + break; + } + + return error; +} + +static int refdb_fs_backend__iterator_next_name( + const char **out, git_reference_iterator *_iter) +{ + int error = GIT_ITEROVER; + refdb_fs_iter *iter = GIT_CONTAINER_OF(_iter, refdb_fs_iter, parent); + refdb_fs_backend *backend = GIT_CONTAINER_OF(iter->parent.db->backend, refdb_fs_backend, parent); + struct packref *ref; + + while (iter->loose_pos < iter->loose.length) { + const char *path = git_vector_get(&iter->loose, iter->loose_pos++); + struct packref *ref; + + if (loose_lookup(NULL, backend, path) == 0) { + ref = git_sortedcache_lookup(iter->cache, path); + if (ref) + ref->flags |= PACKREF_SHADOWED; + + *out = path; + return 0; + } + + git_error_clear(); + } + + error = GIT_ITEROVER; + while (iter->packed_pos < git_sortedcache_entrycount(iter->cache)) { + ref = git_sortedcache_entry(iter->cache, iter->packed_pos++); + if (!ref) /* stop now if another thread deleted refs and we past end */ + break; + + if (ref->flags & PACKREF_SHADOWED) + continue; + if (iter->glob && wildmatch(iter->glob, ref->name, 0) != 0) + continue; + + *out = ref->name; + error = 0; + break; + } + + return error; +} + +static int refdb_fs_backend__iterator( + git_reference_iterator **out, git_refdb_backend *_backend, const char *glob) +{ + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + refdb_fs_iter *iter = NULL; + int error; + + GIT_ASSERT_ARG(backend); + + iter = git__calloc(1, sizeof(refdb_fs_iter)); + GIT_ERROR_CHECK_ALLOC(iter); + + if ((error = git_pool_init(&iter->pool, 1)) < 0) + goto out; + + if ((error = git_vector_init(&iter->loose, 8, NULL)) < 0) + goto out; + + if (glob != NULL && + (iter->glob = git_pool_strdup(&iter->pool, glob)) == NULL) { + error = GIT_ERROR_NOMEMORY; + goto out; + } + + if ((error = iter_load_loose_paths(backend, iter)) < 0) + goto out; + + if ((error = packed_reload(backend)) < 0) + goto out; + + if ((error = git_sortedcache_copy(&iter->cache, backend->refcache, 1, NULL, NULL)) < 0) + goto out; + + iter->parent.next = refdb_fs_backend__iterator_next; + iter->parent.next_name = refdb_fs_backend__iterator_next_name; + iter->parent.free = refdb_fs_backend__iterator_free; + + *out = (git_reference_iterator *)iter; +out: + if (error) + refdb_fs_backend__iterator_free((git_reference_iterator *)iter); + return error; +} + +static bool ref_is_available( + const char *old_ref, const char *new_ref, const char *this_ref) +{ + if (old_ref == NULL || strcmp(old_ref, this_ref)) { + size_t reflen = strlen(this_ref); + size_t newlen = strlen(new_ref); + size_t cmplen = reflen < newlen ? reflen : newlen; + const char *lead = reflen < newlen ? new_ref : this_ref; + + if (!strncmp(new_ref, this_ref, cmplen) && lead[cmplen] == '/') { + return false; + } + } + + return true; +} + +static int reference_path_available( + refdb_fs_backend *backend, + const char *new_ref, + const char *old_ref, + int force) +{ + size_t i; + int error; + + if ((error = packed_reload(backend)) < 0) + return error; + + if (!force) { + int exists; + + if ((error = refdb_fs_backend__exists( + &exists, (git_refdb_backend *)backend, new_ref)) < 0) { + return error; + } + + if (exists) { + git_error_set(GIT_ERROR_REFERENCE, + "failed to write reference '%s': a reference with " + "that name already exists.", new_ref); + return GIT_EEXISTS; + } + } + + if ((error = git_sortedcache_rlock(backend->refcache)) < 0) + return error; + + for (i = 0; i < git_sortedcache_entrycount(backend->refcache); ++i) { + struct packref *ref = git_sortedcache_entry(backend->refcache, i); + + if (ref && !ref_is_available(old_ref, new_ref, ref->name)) { + git_sortedcache_runlock(backend->refcache); + git_error_set(GIT_ERROR_REFERENCE, + "path to reference '%s' collides with existing one", new_ref); + return -1; + } + } + + git_sortedcache_runlock(backend->refcache); + return 0; +} + +static int loose_lock(git_filebuf *file, refdb_fs_backend *backend, const char *name) +{ + int error, filebuf_flags; + git_str ref_path = GIT_STR_INIT; + const char *basedir; + + GIT_ASSERT_ARG(file); + GIT_ASSERT_ARG(backend); + GIT_ASSERT_ARG(name); + + if (!git_path_is_valid(backend->repo, name, 0, GIT_FS_PATH_REJECT_FILESYSTEM_DEFAULTS)) { + git_error_set(GIT_ERROR_INVALID, "invalid reference name '%s'", name); + return GIT_EINVALIDSPEC; + } + + if (is_per_worktree_ref(name)) + basedir = backend->gitpath; + else + basedir = backend->commonpath; + + /* Remove a possibly existing empty directory hierarchy + * which name would collide with the reference name + */ + if ((error = git_futils_rmdir_r(name, basedir, GIT_RMDIR_SKIP_NONEMPTY)) < 0) + return error; + + if ((error = loose_path(&ref_path, basedir, name)) < 0) + return error; + + filebuf_flags = GIT_FILEBUF_CREATE_LEADING_DIRS; + if (backend->fsync) + filebuf_flags |= GIT_FILEBUF_FSYNC; + + error = git_filebuf_open(file, ref_path.ptr, filebuf_flags, GIT_REFS_FILE_MODE); + + if (error == GIT_EDIRECTORY) + git_error_set(GIT_ERROR_REFERENCE, "cannot lock ref '%s', there are refs beneath that folder", name); + + git_str_dispose(&ref_path); + return error; +} + +static int loose_commit(git_filebuf *file, const git_reference *ref) +{ + GIT_ASSERT_ARG(file); + GIT_ASSERT_ARG(ref); + + if (ref->type == GIT_REFERENCE_DIRECT) { + char oid[GIT_OID_MAX_HEXSIZE + 1]; + git_oid_nfmt(oid, sizeof(oid), &ref->target.oid); + + git_filebuf_printf(file, "%s\n", oid); + } else if (ref->type == GIT_REFERENCE_SYMBOLIC) { + git_filebuf_printf(file, GIT_SYMREF "%s\n", ref->target.symbolic); + } else { + GIT_ASSERT(0); + } + + return git_filebuf_commit(file); +} + +static int refdb_fs_backend__lock(void **out, git_refdb_backend *_backend, const char *refname) +{ + int error; + git_filebuf *lock; + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + + lock = git__calloc(1, sizeof(git_filebuf)); + GIT_ERROR_CHECK_ALLOC(lock); + + if ((error = loose_lock(lock, backend, refname)) < 0) { + git__free(lock); + return error; + } + + *out = lock; + return 0; +} + +static int refdb_fs_backend__write_tail( + git_refdb_backend *_backend, + const git_reference *ref, + git_filebuf *file, + int update_reflog, + const git_oid *old_id, + const char *old_target, + const git_signature *who, + const char *message); + +static int refdb_fs_backend__delete_tail( + git_refdb_backend *_backend, + git_filebuf *file, + const char *ref_name, + const git_oid *old_id, + const char *old_target); + +static int refdb_fs_backend__unlock(git_refdb_backend *backend, void *payload, int success, int update_reflog, + const git_reference *ref, const git_signature *sig, const char *message) +{ + git_filebuf *lock = (git_filebuf *) payload; + int error = 0; + + if (success == 2) + error = refdb_fs_backend__delete_tail(backend, lock, ref->name, NULL, NULL); + else if (success) + error = refdb_fs_backend__write_tail(backend, ref, lock, update_reflog, NULL, NULL, sig, message); + else + git_filebuf_cleanup(lock); + + git__free(lock); + return error; +} + +/* + * Find out what object this reference resolves to. + * + * For references that point to a 'big' tag (e.g. an + * actual tag object on the repository), we need to + * cache on the packfile the OID of the object to + * which that 'big tag' is pointing to. + */ +static int packed_find_peel(refdb_fs_backend *backend, struct packref *ref) +{ + git_object *object; + + if (ref->flags & PACKREF_HAS_PEEL || ref->flags & PACKREF_CANNOT_PEEL) + return 0; + + /* + * Find the tagged object in the repository + */ + if (git_object_lookup(&object, backend->repo, &ref->oid, GIT_OBJECT_ANY) < 0) + return -1; + + /* + * If the tagged object is a Tag object, we need to resolve it; + * if the ref is actually a 'weak' ref, we don't need to resolve + * anything. + */ + if (git_object_type(object) == GIT_OBJECT_TAG) { + git_tag *tag = (git_tag *)object; + + /* + * Find the object pointed at by this tag + */ + git_oid_cpy(&ref->peel, git_tag_target_id(tag)); + ref->flags |= PACKREF_HAS_PEEL; + + /* + * The reference has now cached the resolved OID, and is + * marked at such. When written to the packfile, it'll be + * accompanied by this resolved oid + */ + } + + git_object_free(object); + return 0; +} + +/* + * Write a single reference into a packfile + */ +static int packed_write_ref(struct packref *ref, git_filebuf *file) +{ + char oid[GIT_OID_MAX_HEXSIZE + 1]; + git_oid_nfmt(oid, sizeof(oid), &ref->oid); + + /* + * For references that peel to an object in the repo, we must + * write the resulting peel on a separate line, e.g. + * + * 6fa8a902cc1d18527e1355773c86721945475d37 refs/tags/libgit2-0.4 + * ^2ec0cb7959b0bf965d54f95453f5b4b34e8d3100 + * + * This obviously only applies to tags. + * The required peels have already been loaded into `ref->peel_target`. + */ + if (ref->flags & PACKREF_HAS_PEEL) { + char peel[GIT_OID_MAX_HEXSIZE + 1]; + git_oid_nfmt(peel, sizeof(peel), &ref->peel); + + if (git_filebuf_printf(file, "%s %s\n^%s\n", oid, ref->name, peel) < 0) + return -1; + } else { + if (git_filebuf_printf(file, "%s %s\n", oid, ref->name) < 0) + return -1; + } + + return 0; +} + +/* + * Remove all loose references + * + * Once we have successfully written a packfile, + * all the loose references that were packed must be + * removed from disk. + * + * This is a dangerous method; make sure the packfile + * is well-written, because we are destructing references + * here otherwise. + */ +static int packed_remove_loose(refdb_fs_backend *backend) +{ + size_t i; + git_filebuf lock = GIT_FILEBUF_INIT; + git_str ref_content = GIT_STR_INIT; + int error = 0; + + /* backend->refcache is already locked when this is called */ + + for (i = 0; i < git_sortedcache_entrycount(backend->refcache); ++i) { + struct packref *ref = git_sortedcache_entry(backend->refcache, i); + git_oid current_id; + + if (!ref || !(ref->flags & PACKREF_WAS_LOOSE)) + continue; + + git_filebuf_cleanup(&lock); + + /* We need to stop anybody from updating the ref while we try to do a safe delete */ + error = loose_lock(&lock, backend, ref->name); + /* If someone else is updating it, let them do it */ + if (error == GIT_EEXISTS || error == GIT_ENOTFOUND) + continue; + + if (error < 0) { + git_str_dispose(&ref_content); + git_error_set(GIT_ERROR_REFERENCE, "failed to lock loose reference '%s'", ref->name); + return error; + } + + error = git_futils_readbuffer(&ref_content, lock.path_original); + /* Someone else beat us to cleaning up the ref, let's simply continue */ + if (error == GIT_ENOTFOUND) + continue; + + /* This became a symref between us packing and trying to delete it, so ignore it */ + if (!git__prefixcmp(ref_content.ptr, GIT_SYMREF)) + continue; + + /* Figure out the current id; if we find a bad ref file, skip it so we can do the rest */ + if (loose_parse_oid(¤t_id, lock.path_original, &ref_content, backend->oid_type) < 0) + continue; + + /* If the ref moved since we packed it, we must not delete it */ + if (!git_oid_equal(¤t_id, &ref->oid)) + continue; + + /* + * if we fail to remove a single file, this is *not* good, + * but we should keep going and remove as many as possible. + * If we fail to remove, the ref is still in the old state, so + * we haven't lost information. + */ + p_unlink(lock.path_original); + } + + git_str_dispose(&ref_content); + git_filebuf_cleanup(&lock); + return 0; +} + +/* + * Write all the contents in the in-memory packfile to disk. + */ +static int packed_write(refdb_fs_backend *backend) +{ + git_sortedcache *refcache = backend->refcache; + git_filebuf pack_file = GIT_FILEBUF_INIT; + int error, open_flags = 0; + size_t i; + + /* take lock and close up packed-refs mmap if open */ + if ((error = git_mutex_lock(&backend->prlock)) < 0) { + return error; + } + + packed_map_free(backend); + + git_mutex_unlock(&backend->prlock); + + /* lock the cache to updates while we do this */ + if ((error = git_sortedcache_wlock(refcache)) < 0) + return error; + + if (backend->fsync) + open_flags = GIT_FILEBUF_FSYNC; + + /* Open the file! */ + if ((error = git_filebuf_open(&pack_file, git_sortedcache_path(refcache), open_flags, GIT_PACKEDREFS_FILE_MODE)) < 0) + goto fail; + + /* Packfiles have a header... apparently + * This is in fact not required, but we might as well print it + * just for kicks */ + if ((error = git_filebuf_printf(&pack_file, "%s\n", GIT_PACKEDREFS_HEADER)) < 0) + goto fail; + + for (i = 0; i < git_sortedcache_entrycount(refcache); ++i) { + struct packref *ref = git_sortedcache_entry(refcache, i); + + GIT_ASSERT_WITH_CLEANUP(ref, { + error = -1; + goto fail; + }); + + if ((error = packed_find_peel(backend, ref)) < 0) + goto fail; + + if ((error = packed_write_ref(ref, &pack_file)) < 0) + goto fail; + } + + /* if we've written all the references properly, we can commit + * the packfile to make the changes effective */ + if ((error = git_filebuf_commit(&pack_file)) < 0) + goto fail; + + /* when and only when the packfile has been properly written, + * we can go ahead and remove the loose refs */ + if ((error = packed_remove_loose(backend)) < 0) + goto fail; + + git_sortedcache_updated(refcache); + git_sortedcache_wunlock(refcache); + + /* we're good now */ + return 0; + +fail: + git_filebuf_cleanup(&pack_file); + git_sortedcache_wunlock(refcache); + + return error; +} + +static int packed_delete(refdb_fs_backend *backend, const char *ref_name) +{ + size_t pack_pos; + int error, found = 0; + + if ((error = packed_reload(backend)) < 0) + goto cleanup; + + if ((error = git_sortedcache_wlock(backend->refcache)) < 0) + goto cleanup; + + /* If a packed reference exists, remove it from the packfile and repack if necessary */ + error = git_sortedcache_lookup_index(&pack_pos, backend->refcache, ref_name); + if (error == 0) { + error = git_sortedcache_remove(backend->refcache, pack_pos); + found = 1; + } + if (error == GIT_ENOTFOUND) + error = 0; + + git_sortedcache_wunlock(backend->refcache); + + if (found) + error = packed_write(backend); + +cleanup: + return error; +} + +static int reflog_append(refdb_fs_backend *backend, const git_reference *ref, const git_oid *old, const git_oid *new, const git_signature *author, const char *message); + +static int cmp_old_ref(int *cmp, git_refdb_backend *backend, const char *name, + const git_oid *old_id, const char *old_target) +{ + int error = 0; + git_reference *old_ref = NULL; + + *cmp = 0; + /* It "matches" if there is no old value to compare against */ + if (!old_id && !old_target) + return 0; + + if ((error = refdb_fs_backend__lookup(&old_ref, backend, name)) < 0) { + if (error == GIT_ENOTFOUND && old_id && git_oid_is_zero(old_id)) + return 0; + goto out; + } + + /* If the types don't match, there's no way the values do */ + if (old_id && old_ref->type != GIT_REFERENCE_DIRECT) { + *cmp = -1; + goto out; + } + if (old_target && old_ref->type != GIT_REFERENCE_SYMBOLIC) { + *cmp = 1; + goto out; + } + + if (old_id && old_ref->type == GIT_REFERENCE_DIRECT) + *cmp = git_oid_cmp(old_id, &old_ref->target.oid); + + if (old_target && old_ref->type == GIT_REFERENCE_SYMBOLIC) + *cmp = git__strcmp(old_target, old_ref->target.symbolic); + +out: + git_reference_free(old_ref); + + return error; +} + +/* + * The git.git comment regarding this, for your viewing pleasure: + * + * Special hack: If a branch is updated directly and HEAD + * points to it (may happen on the remote side of a push + * for example) then logically the HEAD reflog should be + * updated too. + * A generic solution implies reverse symref information, + * but finding all symrefs pointing to the given branch + * would be rather costly for this rare event (the direct + * update of a branch) to be worth it. So let's cheat and + * check with HEAD only which should cover 99% of all usage + * scenarios (even 100% of the default ones). + */ +static int maybe_append_head(refdb_fs_backend *backend, const git_reference *ref, const git_signature *who, const char *message) +{ + git_reference *head = NULL; + git_refdb *refdb = NULL; + int error, write_reflog; + git_oid old_id; + + if ((error = git_repository_refdb(&refdb, backend->repo)) < 0 || + (error = git_refdb_should_write_head_reflog(&write_reflog, refdb, ref)) < 0) + goto out; + if (!write_reflog) + goto out; + + /* if we can't resolve, we use {0}*40 as old id */ + if (git_reference_name_to_id(&old_id, backend->repo, ref->name) < 0) + memset(&old_id, 0, sizeof(old_id)); + + if ((error = git_reference_lookup(&head, backend->repo, GIT_HEAD_FILE)) < 0 || + (error = reflog_append(backend, head, &old_id, git_reference_target(ref), who, message)) < 0) + goto out; + +out: + git_reference_free(head); + git_refdb_free(refdb); + return error; +} + +static int refdb_fs_backend__write( + git_refdb_backend *_backend, + const git_reference *ref, + int force, + const git_signature *who, + const char *message, + const git_oid *old_id, + const char *old_target) +{ + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + git_filebuf file = GIT_FILEBUF_INIT; + int error = 0; + + GIT_ASSERT_ARG(backend); + + if ((error = reference_path_available(backend, ref->name, NULL, force)) < 0) + return error; + + /* We need to perform the reflog append and old value check under the ref's lock */ + if ((error = loose_lock(&file, backend, ref->name)) < 0) + return error; + + return refdb_fs_backend__write_tail(_backend, ref, &file, true, old_id, old_target, who, message); +} + +static int refdb_fs_backend__write_tail( + git_refdb_backend *_backend, + const git_reference *ref, + git_filebuf *file, + int update_reflog, + const git_oid *old_id, + const char *old_target, + const git_signature *who, + const char *message) +{ + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + int error = 0, cmp = 0, should_write; + const char *new_target = NULL; + const git_oid *new_id = NULL; + + if ((error = cmp_old_ref(&cmp, _backend, ref->name, old_id, old_target)) < 0) + goto on_error; + + if (cmp) { + git_error_set(GIT_ERROR_REFERENCE, "old reference value does not match"); + error = GIT_EMODIFIED; + goto on_error; + } + + if (ref->type == GIT_REFERENCE_SYMBOLIC) + new_target = ref->target.symbolic; + else + new_id = &ref->target.oid; + + error = cmp_old_ref(&cmp, _backend, ref->name, new_id, new_target); + if (error < 0 && error != GIT_ENOTFOUND) + goto on_error; + + /* Don't update if we have the same value */ + if (!error && !cmp) { + error = 0; + goto on_error; /* not really error */ + } + + if (update_reflog) { + git_refdb *refdb; + + if ((error = git_repository_refdb__weakptr(&refdb, backend->repo)) < 0 || + (error = git_refdb_should_write_reflog(&should_write, refdb, ref)) < 0) + goto on_error; + + if (should_write) { + if ((error = reflog_append(backend, ref, NULL, NULL, who, message)) < 0) + goto on_error; + if ((error = maybe_append_head(backend, ref, who, message)) < 0) + goto on_error; + } + } + + return loose_commit(file, ref); + +on_error: + git_filebuf_cleanup(file); + return error; +} + +static int refdb_fs_backend__prune_refs( + refdb_fs_backend *backend, + const char *ref_name, + const char *prefix) +{ + git_str relative_path = GIT_STR_INIT; + git_str base_path = GIT_STR_INIT; + size_t commonlen; + int error; + + GIT_ASSERT_ARG(backend); + GIT_ASSERT_ARG(ref_name); + + if ((error = git_str_sets(&relative_path, ref_name)) < 0) + goto cleanup; + + git_fs_path_squash_slashes(&relative_path); + if ((commonlen = git_fs_path_common_dirlen("refs/heads/", git_str_cstr(&relative_path))) == strlen("refs/heads/") || + (commonlen = git_fs_path_common_dirlen("refs/tags/", git_str_cstr(&relative_path))) == strlen("refs/tags/") || + (commonlen = git_fs_path_common_dirlen("refs/remotes/", git_str_cstr(&relative_path))) == strlen("refs/remotes/")) { + + git_str_truncate(&relative_path, commonlen); + + if (prefix) + error = git_str_join3(&base_path, '/', + backend->commonpath, prefix, + git_str_cstr(&relative_path)); + else + error = git_str_joinpath(&base_path, + backend->commonpath, + git_str_cstr(&relative_path)); + + if (!error) + error = git_path_validate_str_length(NULL, &base_path); + + if (error < 0) + goto cleanup; + + error = git_futils_rmdir_r(ref_name + commonlen, + git_str_cstr(&base_path), + GIT_RMDIR_EMPTY_PARENTS | GIT_RMDIR_SKIP_ROOT); + + if (error == GIT_ENOTFOUND) + error = 0; + } + +cleanup: + git_str_dispose(&relative_path); + git_str_dispose(&base_path); + return error; +} + +static int refdb_fs_backend__delete( + git_refdb_backend *_backend, + const char *ref_name, + const git_oid *old_id, const char *old_target) +{ + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + git_filebuf file = GIT_FILEBUF_INIT; + int error = 0; + + GIT_ASSERT_ARG(backend); + GIT_ASSERT_ARG(ref_name); + + if ((error = loose_lock(&file, backend, ref_name)) < 0) + return error; + + if ((error = refdb_reflog_fs__delete(_backend, ref_name)) < 0) { + git_filebuf_cleanup(&file); + return error; + } + + return refdb_fs_backend__delete_tail(_backend, &file, ref_name, old_id, old_target); +} + +static int loose_delete(refdb_fs_backend *backend, const char *ref_name) +{ + git_str path = GIT_STR_INIT; + int error = 0; + + if ((error = loose_path(&path, backend->commonpath, ref_name)) < 0) + return error; + + error = p_unlink(path.ptr); + if (error < 0 && errno == ENOENT) + error = GIT_ENOTFOUND; + else if (error != 0) + error = -1; + + git_str_dispose(&path); + + return error; +} + +static int refdb_fs_backend__delete_tail( + git_refdb_backend *_backend, + git_filebuf *file, + const char *ref_name, + const git_oid *old_id, const char *old_target) +{ + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + int error = 0, cmp = 0; + bool packed_deleted = 0; + + error = cmp_old_ref(&cmp, _backend, ref_name, old_id, old_target); + if (error < 0) + goto cleanup; + + if (cmp) { + git_error_set(GIT_ERROR_REFERENCE, "old reference value does not match"); + error = GIT_EMODIFIED; + goto cleanup; + } + + /* + * To ensure that an external observer will see either the current ref value + * (because the loose ref still exists), or a missing ref (after the packed-file is + * unlocked, there will be nothing left), we must ensure things happen in the + * following order: + * + * - the packed-ref file is locked and loaded, as well as a loose one, if it exists + * - we optimistically delete a packed ref, keeping track of whether it existed + * - we delete the loose ref, note that we have its .lock + * - the loose ref is "unlocked", then the packed-ref file is rewritten and unlocked + * - we should prune the path components if a loose ref was deleted + * + * Note that, because our packed backend doesn't expose its filesystem lock, + * we might not be able to guarantee that this is what actually happens (ie. + * as our current code never write packed-refs.lock, nothing stops observers + * from grabbing a "stale" value from there). + */ + if ((error = packed_delete(backend, ref_name)) < 0 && error != GIT_ENOTFOUND) + goto cleanup; + + if (error == 0) + packed_deleted = 1; + + if ((error = loose_delete(backend, ref_name)) < 0 && error != GIT_ENOTFOUND) + goto cleanup; + + if (error == GIT_ENOTFOUND) { + error = packed_deleted ? 0 : ref_error_notfound(ref_name); + goto cleanup; + } + +cleanup: + git_filebuf_cleanup(file); + if (error == 0) + error = refdb_fs_backend__prune_refs(backend, ref_name, ""); + return error; +} + +static int refdb_reflog_fs__rename(git_refdb_backend *_backend, const char *old_name, const char *new_name); + +static int refdb_fs_backend__rename( + git_reference **out, + git_refdb_backend *_backend, + const char *old_name, + const char *new_name, + int force, + const git_signature *who, + const char *message) +{ + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + git_reference *old, *new = NULL; + git_filebuf file = GIT_FILEBUF_INIT; + int error; + + GIT_ASSERT_ARG(backend); + + if ((error = reference_path_available( + backend, new_name, old_name, force)) < 0 || + (error = refdb_fs_backend__lookup(&old, _backend, old_name)) < 0) + return error; + + if ((error = loose_lock(&file, backend, old->name)) < 0) { + git_reference_free(old); + return error; + } + + new = git_reference__realloc(&old, new_name); + if (!new) { + git_reference_free(old); + git_filebuf_cleanup(&file); + return -1; + } + + if ((error = refdb_fs_backend__delete_tail(_backend, &file, old_name, NULL, NULL)) < 0) { + git_reference_free(new); + git_filebuf_cleanup(&file); + return error; + } + + if ((error = loose_lock(&file, backend, new_name)) < 0) { + git_reference_free(new); + return error; + } + + /* Try to rename the refog; it's ok if the old doesn't exist */ + error = refdb_reflog_fs__rename(_backend, old_name, new_name); + if (((error == 0) || (error == GIT_ENOTFOUND)) && + ((error = reflog_append(backend, new, git_reference_target(new), NULL, who, message)) < 0)) { + git_reference_free(new); + git_filebuf_cleanup(&file); + return error; + } + + if ((error = loose_commit(&file, new)) < 0 || out == NULL) { + git_reference_free(new); + git_filebuf_cleanup(&file); + return error; + } + + *out = new; + return 0; +} + +static int refdb_fs_backend__compress(git_refdb_backend *_backend) +{ + int error; + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + + GIT_ASSERT_ARG(backend); + + if ((error = packed_reload(backend)) < 0 || /* load the existing packfile */ + (error = packed_loadloose(backend)) < 0 || /* add all the loose refs */ + (error = packed_write(backend)) < 0) /* write back to disk */ + return error; + + return 0; +} + +static void refdb_fs_backend__free(git_refdb_backend *_backend) +{ + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + + if (!backend) + return; + + git_sortedcache_free(backend->refcache); + + git_mutex_lock(&backend->prlock); + packed_map_free(backend); + git_mutex_unlock(&backend->prlock); + git_mutex_free(&backend->prlock); + + git__free(backend->gitpath); + git__free(backend->commonpath); + git__free(backend); +} + +static char *setup_namespace(git_repository *repo, const char *in) +{ + git_str path = GIT_STR_INIT; + char *parts, *start, *end, *out = NULL; + + if (!in) + goto done; + + git_str_puts(&path, in); + + /* if the repo is not namespaced, nothing else to do */ + if (repo->namespace == NULL) { + out = git_str_detach(&path); + goto done; + } + + parts = end = git__strdup(repo->namespace); + if (parts == NULL) + goto done; + + /* + * From `man gitnamespaces`: + * namespaces which include a / will expand to a hierarchy + * of namespaces; for example, GIT_NAMESPACE=foo/bar will store + * refs under refs/namespaces/foo/refs/namespaces/bar/ + */ + while ((start = git__strsep(&end, "/")) != NULL) + git_str_printf(&path, "refs/namespaces/%s/", start); + + git_str_printf(&path, "refs/namespaces/%s/refs", end); + git__free(parts); + + /* Make sure that the folder with the namespace exists */ + if (git_futils_mkdir_relative(git_str_cstr(&path), in, 0777, + GIT_MKDIR_PATH, NULL) < 0) + goto done; + + /* Return root of the namespaced gitpath, i.e. without the trailing 'refs' */ + git_str_rtruncate_at_char(&path, '/'); + git_str_putc(&path, '/'); + out = git_str_detach(&path); + +done: + git_str_dispose(&path); + return out; +} + +static int reflog_alloc( + git_reflog **reflog, + const char *name, + git_oid_t oid_type) +{ + git_reflog *log; + + *reflog = NULL; + + log = git__calloc(1, sizeof(git_reflog)); + GIT_ERROR_CHECK_ALLOC(log); + + log->ref_name = git__strdup(name); + GIT_ERROR_CHECK_ALLOC(log->ref_name); + + log->oid_type = oid_type; + + if (git_vector_init(&log->entries, 0, NULL) < 0) { + git__free(log->ref_name); + git__free(log); + return -1; + } + + *reflog = log; + + return 0; +} + +static int reflog_parse(git_reflog *log, const char *buf, size_t buf_size) +{ + git_parse_ctx parser = GIT_PARSE_CTX_INIT; + + if ((git_parse_ctx_init(&parser, buf, buf_size)) < 0) + return -1; + + for (; parser.remain_len; git_parse_advance_line(&parser)) { + git_reflog_entry *entry; + const char *sig; + char c; + + entry = git__calloc(1, sizeof(*entry)); + GIT_ERROR_CHECK_ALLOC(entry); + entry->committer = git__calloc(1, sizeof(*entry->committer)); + GIT_ERROR_CHECK_ALLOC(entry->committer); + + if (git_parse_advance_oid(&entry->oid_old, &parser, log->oid_type) < 0 || + git_parse_advance_expected(&parser, " ", 1) < 0 || + git_parse_advance_oid(&entry->oid_cur, &parser, log->oid_type) < 0) + goto next; + + sig = parser.line; + while (git_parse_peek(&c, &parser, 0) == 0 && c != '\t' && c != '\n') + git_parse_advance_chars(&parser, 1); + + if (git_signature__parse(entry->committer, &sig, parser.line, NULL, 0) < 0) + goto next; + + if (c == '\t') { + size_t len; + git_parse_advance_chars(&parser, 1); + + len = parser.line_len; + if (parser.line[len - 1] == '\n') + len--; + + entry->msg = git__strndup(parser.line, len); + GIT_ERROR_CHECK_ALLOC(entry->msg); + } + + if ((git_vector_insert(&log->entries, entry)) < 0) { + git_reflog_entry__free(entry); + return -1; + } + + continue; + +next: + git_reflog_entry__free(entry); + } + + return 0; +} + +static int create_new_reflog_file(const char *filepath) +{ + int fd, error; + + if ((error = git_futils_mkpath2file(filepath, GIT_REFLOG_DIR_MODE)) < 0) + return error; + + if ((fd = p_open(filepath, + O_WRONLY | O_CREAT, + GIT_REFLOG_FILE_MODE)) < 0) + return -1; + + return p_close(fd); +} + +static int refdb_reflog_fs__ensure_log(git_refdb_backend *_backend, const char *name) +{ + refdb_fs_backend *backend; + git_repository *repo; + git_str path = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(_backend && name); + + backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + repo = backend->repo; + + if ((error = reflog_path(&path, repo, name)) < 0) + return error; + + error = create_new_reflog_file(git_str_cstr(&path)); + git_str_dispose(&path); + + return error; +} + +static int has_reflog(git_repository *repo, const char *name) +{ + int ret = 0; + git_str path = GIT_STR_INIT; + + if (reflog_path(&path, repo, name) < 0) + goto cleanup; + + ret = git_fs_path_isfile(git_str_cstr(&path)); + +cleanup: + git_str_dispose(&path); + return ret; +} + +static int refdb_reflog_fs__has_log(git_refdb_backend *_backend, const char *name) +{ + refdb_fs_backend *backend; + + GIT_ASSERT_ARG(_backend); + GIT_ASSERT_ARG(name); + + backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + + return has_reflog(backend->repo, name); +} + +static int refdb_reflog_fs__read( + git_reflog **out, + git_refdb_backend *_backend, + const char *name) +{ + int error = -1; + git_str log_path = GIT_STR_INIT; + git_str log_file = GIT_STR_INIT; + git_reflog *log = NULL; + git_repository *repo; + refdb_fs_backend *backend; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(_backend); + GIT_ASSERT_ARG(name); + + backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + repo = backend->repo; + + if (reflog_alloc(&log, name, backend->oid_type) < 0) + return -1; + + if (reflog_path(&log_path, repo, name) < 0) + goto cleanup; + + error = git_futils_readbuffer(&log_file, git_str_cstr(&log_path)); + if (error < 0 && error != GIT_ENOTFOUND) + goto cleanup; + + if ((error == GIT_ENOTFOUND) && + ((error = create_new_reflog_file(git_str_cstr(&log_path))) < 0)) + goto cleanup; + + if ((error = reflog_parse(log, + git_str_cstr(&log_file), git_str_len(&log_file))) < 0) + goto cleanup; + + *out = log; + goto success; + +cleanup: + git_reflog_free(log); + +success: + git_str_dispose(&log_file); + git_str_dispose(&log_path); + + return error; +} + +static int serialize_reflog_entry( + git_str *buf, + const git_oid *oid_old, + const git_oid *oid_new, + const git_signature *committer, + const char *msg) +{ + char raw_old[GIT_OID_MAX_HEXSIZE + 1]; + char raw_new[GIT_OID_MAX_HEXSIZE + 1]; + + git_oid_tostr(raw_old, GIT_OID_MAX_HEXSIZE + 1, oid_old); + git_oid_tostr(raw_new, GIT_OID_MAX_HEXSIZE + 1, oid_new); + + git_str_clear(buf); + + git_str_puts(buf, raw_old); + git_str_putc(buf, ' '); + git_str_puts(buf, raw_new); + + git_signature__writebuf(buf, " ", committer); + + /* drop trailing LF */ + git_str_rtrim(buf); + + if (msg) { + size_t i; + + git_str_putc(buf, '\t'); + git_str_puts(buf, msg); + + for (i = 0; i < buf->size - 2; i++) + if (buf->ptr[i] == '\n') + buf->ptr[i] = ' '; + git_str_rtrim(buf); + } + + git_str_putc(buf, '\n'); + + return git_str_oom(buf); +} + +static int lock_reflog(git_filebuf *file, refdb_fs_backend *backend, const char *refname) +{ + git_repository *repo; + git_str log_path = GIT_STR_INIT; + int error; + + repo = backend->repo; + + if (!git_path_is_valid(backend->repo, refname, 0, GIT_FS_PATH_REJECT_FILESYSTEM_DEFAULTS)) { + git_error_set(GIT_ERROR_INVALID, "invalid reference name '%s'", refname); + return GIT_EINVALIDSPEC; + } + + if (reflog_path(&log_path, repo, refname) < 0) + return -1; + + if (!git_fs_path_isfile(git_str_cstr(&log_path))) { + git_error_set(GIT_ERROR_INVALID, + "log file for reference '%s' doesn't exist", refname); + error = -1; + goto cleanup; + } + + error = git_filebuf_open(file, git_str_cstr(&log_path), 0, GIT_REFLOG_FILE_MODE); + +cleanup: + git_str_dispose(&log_path); + + return error; +} + +static int refdb_reflog_fs__write(git_refdb_backend *_backend, git_reflog *reflog) +{ + int error = -1; + unsigned int i; + git_reflog_entry *entry; + refdb_fs_backend *backend; + git_str log = GIT_STR_INIT; + git_filebuf fbuf = GIT_FILEBUF_INIT; + + GIT_ASSERT_ARG(_backend); + GIT_ASSERT_ARG(reflog); + + backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + + if ((error = lock_reflog(&fbuf, backend, reflog->ref_name)) < 0) + return -1; + + git_vector_foreach(&reflog->entries, i, entry) { + if (serialize_reflog_entry(&log, &(entry->oid_old), &(entry->oid_cur), entry->committer, entry->msg) < 0) + goto cleanup; + + if ((error = git_filebuf_write(&fbuf, log.ptr, log.size)) < 0) + goto cleanup; + } + + error = git_filebuf_commit(&fbuf); + goto success; + +cleanup: + git_filebuf_cleanup(&fbuf); + +success: + git_str_dispose(&log); + + return error; +} + +/* Append to the reflog, must be called under reference lock */ +static int reflog_append( + refdb_fs_backend *backend, + const git_reference *ref, + const git_oid *old, + const git_oid *new, + const git_signature *who, + const char *message) +{ + int error, is_symbolic, open_flags; + git_oid old_id, new_id; + git_str buf = GIT_STR_INIT, path = GIT_STR_INIT; + git_repository *repo = backend->repo; + + is_symbolic = ref->type == GIT_REFERENCE_SYMBOLIC; + + /* "normal" symbolic updates do not write */ + if (is_symbolic && + strcmp(ref->name, GIT_HEAD_FILE) && + !(old && new)) + return 0; + + /* From here on is_symbolic also means that it's HEAD */ + + git_oid_clear(&old_id, backend->oid_type); + git_oid_clear(&new_id, backend->oid_type); + + if (old) { + git_oid_cpy(&old_id, old); + } else { + error = git_reference_name_to_id(&old_id, repo, ref->name); + if (error < 0 && error != GIT_ENOTFOUND) + return error; + } + + if (new) { + git_oid_cpy(&new_id, new); + } else { + if (!is_symbolic) { + git_oid_cpy(&new_id, git_reference_target(ref)); + } else { + error = git_reference_name_to_id(&new_id, repo, git_reference_symbolic_target(ref)); + if (error < 0 && error != GIT_ENOTFOUND) + return error; + /* detaching HEAD does not create an entry */ + if (error == GIT_ENOTFOUND) + return 0; + + git_error_clear(); + } + } + + if ((error = serialize_reflog_entry(&buf, &old_id, &new_id, who, message)) < 0) + goto cleanup; + + if ((error = reflog_path(&path, repo, ref->name)) < 0) + goto cleanup; + + if (((error = git_futils_mkpath2file(git_str_cstr(&path), 0777)) < 0) && + (error != GIT_EEXISTS)) { + goto cleanup; + } + + /* If the new branch matches part of the namespace of a previously deleted branch, + * there maybe an obsolete/unused directory (or directory hierarchy) in the way. + */ + if (git_fs_path_isdir(git_str_cstr(&path))) { + if ((error = git_futils_rmdir_r(git_str_cstr(&path), NULL, GIT_RMDIR_SKIP_NONEMPTY)) < 0) { + if (error == GIT_ENOTFOUND) + error = 0; + } else if (git_fs_path_isdir(git_str_cstr(&path))) { + git_error_set(GIT_ERROR_REFERENCE, "cannot create reflog at '%s', there are reflogs beneath that folder", + ref->name); + error = GIT_EDIRECTORY; + } + + if (error != 0) + goto cleanup; + } + + open_flags = O_WRONLY | O_CREAT | O_APPEND; + + if (backend->fsync) + open_flags |= O_FSYNC; + + error = git_futils_writebuffer(&buf, git_str_cstr(&path), open_flags, GIT_REFLOG_FILE_MODE); + +cleanup: + git_str_dispose(&buf); + git_str_dispose(&path); + + return error; +} + +static int refdb_reflog_fs__rename(git_refdb_backend *_backend, const char *old_name, const char *new_name) +{ + int error = 0, fd; + git_str old_path = GIT_STR_INIT; + git_str new_path = GIT_STR_INIT; + git_str temp_path = GIT_STR_INIT; + git_str normalized = GIT_STR_INIT; + git_repository *repo; + refdb_fs_backend *backend; + + GIT_ASSERT_ARG(_backend); + GIT_ASSERT_ARG(old_name); + GIT_ASSERT_ARG(new_name); + + backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + repo = backend->repo; + + if ((error = git_reference__normalize_name( + &normalized, new_name, GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL)) < 0) + return error; + + if (git_str_joinpath(&temp_path, repo->gitdir, GIT_REFLOG_DIR) < 0) + return -1; + + if ((error = loose_path(&old_path, git_str_cstr(&temp_path), old_name)) < 0) + return error; + + if ((error = loose_path(&new_path, git_str_cstr(&temp_path), git_str_cstr(&normalized))) < 0) + return error; + + if (!git_fs_path_exists(git_str_cstr(&old_path))) { + error = GIT_ENOTFOUND; + goto cleanup; + } + + /* + * Move the reflog to a temporary place. This two-phase renaming is required + * in order to cope with funny renaming use cases when one tries to move a reference + * to a partially colliding namespace: + * - a/b -> a/b/c + * - a/b/c/d -> a/b/c + */ + if ((error = loose_path(&temp_path, git_str_cstr(&temp_path), "temp_reflog")) < 0) + return error; + + if ((fd = git_futils_mktmp(&temp_path, git_str_cstr(&temp_path), GIT_REFLOG_FILE_MODE)) < 0) { + error = -1; + goto cleanup; + } + + p_close(fd); + + if (p_rename(git_str_cstr(&old_path), git_str_cstr(&temp_path)) < 0) { + git_error_set(GIT_ERROR_OS, "failed to rename reflog for %s", new_name); + error = -1; + goto cleanup; + } + + if (git_fs_path_isdir(git_str_cstr(&new_path)) && + (git_futils_rmdir_r(git_str_cstr(&new_path), NULL, GIT_RMDIR_SKIP_NONEMPTY) < 0)) { + error = -1; + goto cleanup; + } + + if (git_futils_mkpath2file(git_str_cstr(&new_path), GIT_REFLOG_DIR_MODE) < 0) { + error = -1; + goto cleanup; + } + + if (p_rename(git_str_cstr(&temp_path), git_str_cstr(&new_path)) < 0) { + git_error_set(GIT_ERROR_OS, "failed to rename reflog for %s", new_name); + error = -1; + } + +cleanup: + git_str_dispose(&temp_path); + git_str_dispose(&old_path); + git_str_dispose(&new_path); + git_str_dispose(&normalized); + + return error; +} + +static int refdb_reflog_fs__delete(git_refdb_backend *_backend, const char *name) +{ + refdb_fs_backend *backend = GIT_CONTAINER_OF(_backend, refdb_fs_backend, parent); + git_str path = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(_backend); + GIT_ASSERT_ARG(name); + + if ((error = reflog_path(&path, backend->repo, name)) < 0) + goto out; + + /* + * If a reference was moved downwards, eg refs/heads/br2 -> refs/heads/br2/new-name, + * refs/heads/br2 does exist but it's a directory. That's a valid situation. + * Proceed only if it's a file. + */ + if (!git_fs_path_isfile(path.ptr)) + goto out; + + if ((error = p_unlink(path.ptr)) < 0) + goto out; + + error = refdb_fs_backend__prune_refs(backend, name, GIT_REFLOG_DIR); + +out: + git_str_dispose(&path); + + return error; +} + +int git_refdb_backend_fs( + git_refdb_backend **backend_out, + git_repository *repository) +{ + int t = 0; + git_str gitpath = GIT_STR_INIT; + refdb_fs_backend *backend; + + backend = git__calloc(1, sizeof(refdb_fs_backend)); + GIT_ERROR_CHECK_ALLOC(backend); + if (git_mutex_init(&backend->prlock) < 0) { + git__free(backend); + return -1; + } + + + if (git_refdb_init_backend(&backend->parent, GIT_REFDB_BACKEND_VERSION) < 0) + goto fail; + + backend->repo = repository; + backend->oid_type = repository->oid_type; + + if (repository->gitdir) { + backend->gitpath = setup_namespace(repository, repository->gitdir); + + if (backend->gitpath == NULL) + goto fail; + } + + if (repository->commondir) { + backend->commonpath = setup_namespace(repository, repository->commondir); + + if (backend->commonpath == NULL) + goto fail; + } + + if (git_str_joinpath(&gitpath, backend->commonpath, GIT_PACKEDREFS_FILE) < 0 || + git_sortedcache_new( + &backend->refcache, offsetof(struct packref, name), + NULL, NULL, packref_cmp, git_str_cstr(&gitpath)) < 0) + goto fail; + + git_str_dispose(&gitpath); + + if (!git_repository__configmap_lookup(&t, backend->repo, GIT_CONFIGMAP_IGNORECASE) && t) { + backend->iterator_flags |= GIT_ITERATOR_IGNORE_CASE; + backend->direach_flags |= GIT_FS_PATH_DIR_IGNORE_CASE; + } + if (!git_repository__configmap_lookup(&t, backend->repo, GIT_CONFIGMAP_PRECOMPOSE) && t) { + backend->iterator_flags |= GIT_ITERATOR_PRECOMPOSE_UNICODE; + backend->direach_flags |= GIT_FS_PATH_DIR_PRECOMPOSE_UNICODE; + } + if ((!git_repository__configmap_lookup(&t, backend->repo, GIT_CONFIGMAP_FSYNCOBJECTFILES) && t) || + git_repository__fsync_gitdir) + backend->fsync = 1; + backend->iterator_flags |= GIT_ITERATOR_DESCEND_SYMLINKS; + + backend->parent.exists = &refdb_fs_backend__exists; + backend->parent.lookup = &refdb_fs_backend__lookup; + backend->parent.iterator = &refdb_fs_backend__iterator; + backend->parent.write = &refdb_fs_backend__write; + backend->parent.del = &refdb_fs_backend__delete; + backend->parent.rename = &refdb_fs_backend__rename; + backend->parent.compress = &refdb_fs_backend__compress; + backend->parent.lock = &refdb_fs_backend__lock; + backend->parent.unlock = &refdb_fs_backend__unlock; + backend->parent.has_log = &refdb_reflog_fs__has_log; + backend->parent.ensure_log = &refdb_reflog_fs__ensure_log; + backend->parent.free = &refdb_fs_backend__free; + backend->parent.reflog_read = &refdb_reflog_fs__read; + backend->parent.reflog_write = &refdb_reflog_fs__write; + backend->parent.reflog_rename = &refdb_reflog_fs__rename; + backend->parent.reflog_delete = &refdb_reflog_fs__delete; + + *backend_out = (git_refdb_backend *)backend; + return 0; + +fail: + git_mutex_free(&backend->prlock); + git_str_dispose(&gitpath); + git__free(backend->gitpath); + git__free(backend->commonpath); + git__free(backend); + return -1; +} diff --git a/src/libgit2/reflog.c b/src/libgit2/reflog.c new file mode 100644 index 0000000..86d4355 --- /dev/null +++ b/src/libgit2/reflog.c @@ -0,0 +1,234 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "reflog.h" + +#include "repository.h" +#include "filebuf.h" +#include "signature.h" +#include "refdb.h" + +#include "git2/sys/refdb_backend.h" +#include "git2/sys/reflog.h" + +void git_reflog_entry__free(git_reflog_entry *entry) +{ + git_signature_free(entry->committer); + + git__free(entry->msg); + git__free(entry); +} + +void git_reflog_free(git_reflog *reflog) +{ + size_t i; + git_reflog_entry *entry; + + if (reflog == NULL) + return; + + if (reflog->db) + GIT_REFCOUNT_DEC(reflog->db, git_refdb__free); + + for (i=0; i < reflog->entries.length; i++) { + entry = git_vector_get(&reflog->entries, i); + + git_reflog_entry__free(entry); + } + + git_vector_free(&reflog->entries); + git__free(reflog->ref_name); + git__free(reflog); +} + +int git_reflog_read(git_reflog **reflog, git_repository *repo, const char *name) +{ + git_refdb *refdb; + int error; + + GIT_ASSERT_ARG(reflog); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + if ((error = git_repository_refdb__weakptr(&refdb, repo)) < 0) + return error; + + return git_refdb_reflog_read(reflog, refdb, name); +} + +int git_reflog_write(git_reflog *reflog) +{ + git_refdb *db; + + GIT_ASSERT_ARG(reflog); + GIT_ASSERT_ARG(reflog->db); + + db = reflog->db; + return db->backend->reflog_write(db->backend, reflog); +} + +int git_reflog_append( + git_reflog *reflog, + const git_oid *new_oid, + const git_signature *committer, + const char *msg) +{ + const git_reflog_entry *previous; + git_reflog_entry *entry; + + GIT_ASSERT_ARG(reflog); + GIT_ASSERT_ARG(new_oid); + GIT_ASSERT_ARG(committer); + + entry = git__calloc(1, sizeof(git_reflog_entry)); + GIT_ERROR_CHECK_ALLOC(entry); + + if ((git_signature_dup(&entry->committer, committer)) < 0) + goto cleanup; + + if (msg != NULL) { + size_t i, msglen = strlen(msg); + + if ((entry->msg = git__strndup(msg, msglen)) == NULL) + goto cleanup; + + /* + * Replace all newlines with spaces, except for + * the final trailing newline. + */ + for (i = 0; i < msglen; i++) + if (entry->msg[i] == '\n') + entry->msg[i] = ' '; + } + + previous = git_reflog_entry_byindex(reflog, 0); + + if (previous == NULL) + git_oid_clear(&entry->oid_old, reflog->oid_type); + else + git_oid_cpy(&entry->oid_old, &previous->oid_cur); + + git_oid_cpy(&entry->oid_cur, new_oid); + + if (git_vector_insert(&reflog->entries, entry) < 0) + goto cleanup; + + return 0; + +cleanup: + git_reflog_entry__free(entry); + return -1; +} + +int git_reflog_rename(git_repository *repo, const char *old_name, const char *new_name) +{ + git_refdb *refdb; + int error; + + if ((error = git_repository_refdb__weakptr(&refdb, repo)) < 0) + return -1; + + return refdb->backend->reflog_rename(refdb->backend, old_name, new_name); +} + +int git_reflog_delete(git_repository *repo, const char *name) +{ + git_refdb *refdb; + int error; + + if ((error = git_repository_refdb__weakptr(&refdb, repo)) < 0) + return -1; + + return refdb->backend->reflog_delete(refdb->backend, name); +} + +size_t git_reflog_entrycount(git_reflog *reflog) +{ + GIT_ASSERT_ARG_WITH_RETVAL(reflog, 0); + return reflog->entries.length; +} + +const git_reflog_entry *git_reflog_entry_byindex(const git_reflog *reflog, size_t idx) +{ + GIT_ASSERT_ARG_WITH_RETVAL(reflog, NULL); + + if (idx >= reflog->entries.length) + return NULL; + + return git_vector_get( + &reflog->entries, reflog_inverse_index(idx, reflog->entries.length)); +} + +const git_oid *git_reflog_entry_id_old(const git_reflog_entry *entry) +{ + GIT_ASSERT_ARG_WITH_RETVAL(entry, NULL); + return &entry->oid_old; +} + +const git_oid *git_reflog_entry_id_new(const git_reflog_entry *entry) +{ + GIT_ASSERT_ARG_WITH_RETVAL(entry, NULL); + return &entry->oid_cur; +} + +const git_signature *git_reflog_entry_committer(const git_reflog_entry *entry) +{ + GIT_ASSERT_ARG_WITH_RETVAL(entry, NULL); + return entry->committer; +} + +const char *git_reflog_entry_message(const git_reflog_entry *entry) +{ + GIT_ASSERT_ARG_WITH_RETVAL(entry, NULL); + return entry->msg; +} + +int git_reflog_drop(git_reflog *reflog, size_t idx, int rewrite_previous_entry) +{ + size_t entrycount; + git_reflog_entry *entry, *previous; + + entrycount = git_reflog_entrycount(reflog); + + entry = (git_reflog_entry *)git_reflog_entry_byindex(reflog, idx); + + if (entry == NULL) { + git_error_set(GIT_ERROR_REFERENCE, "no reflog entry at index %"PRIuZ, idx); + return GIT_ENOTFOUND; + } + + git_reflog_entry__free(entry); + + if (git_vector_remove( + &reflog->entries, reflog_inverse_index(idx, entrycount)) < 0) + return -1; + + if (!rewrite_previous_entry) + return 0; + + /* No need to rewrite anything when removing the most recent entry */ + if (idx == 0) + return 0; + + /* Have the latest entry just been dropped? */ + if (entrycount == 1) + return 0; + + entry = (git_reflog_entry *)git_reflog_entry_byindex(reflog, idx - 1); + + /* If the oldest entry has just been removed... */ + if (idx == entrycount - 1) { + /* ...clear the oid_old member of the "new" oldest entry */ + git_oid_clear(&entry->oid_old, reflog->oid_type); + return 0; + } + + previous = (git_reflog_entry *)git_reflog_entry_byindex(reflog, idx); + git_oid_cpy(&entry->oid_old, &previous->oid_cur); + + return 0; +} diff --git a/src/libgit2/reflog.h b/src/libgit2/reflog.h new file mode 100644 index 0000000..bc98785 --- /dev/null +++ b/src/libgit2/reflog.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_reflog_h__ +#define INCLUDE_reflog_h__ + +#include "common.h" + +#include "git2/reflog.h" +#include "vector.h" + +#define GIT_REFLOG_DIR "logs/" +#define GIT_REFLOG_DIR_MODE 0777 +#define GIT_REFLOG_FILE_MODE 0666 + +struct git_reflog_entry { + git_oid oid_old; + git_oid oid_cur; + + git_signature *committer; + + char *msg; +}; + +struct git_reflog { + git_refdb *db; + char *ref_name; + git_oid_t oid_type; + git_vector entries; +}; + +GIT_INLINE(size_t) reflog_inverse_index(size_t idx, size_t total) +{ + return (total - 1) - idx; +} + +#endif diff --git a/src/libgit2/refs.c b/src/libgit2/refs.c new file mode 100644 index 0000000..5e2fe97 --- /dev/null +++ b/src/libgit2/refs.c @@ -0,0 +1,1404 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "refs.h" + +#include "hash.h" +#include "repository.h" +#include "futils.h" +#include "filebuf.h" +#include "pack.h" +#include "reflog.h" +#include "refdb.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +bool git_reference__enable_symbolic_ref_target_validation = true; + +enum { + GIT_PACKREF_HAS_PEEL = 1, + GIT_PACKREF_WAS_LOOSE = 2 +}; + +static git_reference *alloc_ref(const char *name) +{ + git_reference *ref = NULL; + size_t namelen = strlen(name), reflen; + + if (!GIT_ADD_SIZET_OVERFLOW(&reflen, sizeof(git_reference), namelen) && + !GIT_ADD_SIZET_OVERFLOW(&reflen, reflen, 1) && + (ref = git__calloc(1, reflen)) != NULL) + memcpy(ref->name, name, namelen + 1); + + return ref; +} + +git_reference *git_reference__alloc_symbolic( + const char *name, const char *target) +{ + git_reference *ref; + + GIT_ASSERT_ARG_WITH_RETVAL(name, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(target, NULL); + + ref = alloc_ref(name); + if (!ref) + return NULL; + + ref->type = GIT_REFERENCE_SYMBOLIC; + + if ((ref->target.symbolic = git__strdup(target)) == NULL) { + git__free(ref); + return NULL; + } + + return ref; +} + +git_reference *git_reference__alloc( + const char *name, + const git_oid *oid, + const git_oid *peel) +{ + git_oid_t oid_type; + git_reference *ref; + + GIT_ASSERT_ARG_WITH_RETVAL(name, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(oid, NULL); + + ref = alloc_ref(name); + if (!ref) + return NULL; + + ref->type = GIT_REFERENCE_DIRECT; + git_oid_cpy(&ref->target.oid, oid); + +#ifdef GIT_EXPERIMENTAL_SHA256 + oid_type = oid->type; +#else + oid_type = GIT_OID_SHA1; +#endif + + if (peel != NULL) + git_oid_cpy(&ref->peel, peel); + else + git_oid_clear(&ref->peel, oid_type); + + return ref; +} + +git_reference *git_reference__realloc( + git_reference **ptr_to_ref, const char *name) +{ + size_t namelen, reflen; + git_reference *rewrite = NULL; + + GIT_ASSERT_ARG_WITH_RETVAL(ptr_to_ref, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(name, NULL); + + namelen = strlen(name); + + if (!GIT_ADD_SIZET_OVERFLOW(&reflen, sizeof(git_reference), namelen) && + !GIT_ADD_SIZET_OVERFLOW(&reflen, reflen, 1) && + (rewrite = git__realloc(*ptr_to_ref, reflen)) != NULL) + memcpy(rewrite->name, name, namelen + 1); + + *ptr_to_ref = NULL; + + return rewrite; +} + +int git_reference_dup(git_reference **dest, git_reference *source) +{ + if (source->type == GIT_REFERENCE_SYMBOLIC) + *dest = git_reference__alloc_symbolic(source->name, source->target.symbolic); + else + *dest = git_reference__alloc(source->name, &source->target.oid, &source->peel); + + GIT_ERROR_CHECK_ALLOC(*dest); + + (*dest)->db = source->db; + GIT_REFCOUNT_INC((*dest)->db); + + return 0; +} + +void git_reference_free(git_reference *reference) +{ + if (reference == NULL) + return; + + if (reference->type == GIT_REFERENCE_SYMBOLIC) + git__free(reference->target.symbolic); + + if (reference->db) + GIT_REFCOUNT_DEC(reference->db, git_refdb__free); + + git__free(reference); +} + +int git_reference_delete(git_reference *ref) +{ + const git_oid *old_id = NULL; + const char *old_target = NULL; + + if (!strcmp(ref->name, "HEAD")) { + git_error_set(GIT_ERROR_REFERENCE, "cannot delete HEAD"); + return GIT_ERROR; + } + + if (ref->type == GIT_REFERENCE_DIRECT) + old_id = &ref->target.oid; + else + old_target = ref->target.symbolic; + + return git_refdb_delete(ref->db, ref->name, old_id, old_target); +} + +int git_reference_remove(git_repository *repo, const char *name) +{ + git_refdb *db; + int error; + + if ((error = git_repository_refdb__weakptr(&db, repo)) < 0) + return error; + + return git_refdb_delete(db, name, NULL, NULL); +} + +int git_reference_lookup(git_reference **ref_out, + git_repository *repo, const char *name) +{ + return git_reference_lookup_resolved(ref_out, repo, name, 0); +} + +int git_reference_name_to_id( + git_oid *out, git_repository *repo, const char *name) +{ + int error; + git_reference *ref; + + if ((error = git_reference_lookup_resolved(&ref, repo, name, -1)) < 0) + return error; + + git_oid_cpy(out, git_reference_target(ref)); + git_reference_free(ref); + return 0; +} + +static int reference_normalize_for_repo( + git_refname_t out, + git_repository *repo, + const char *name, + bool validate) +{ + int precompose; + unsigned int flags = GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL; + + if (!git_repository__configmap_lookup(&precompose, repo, GIT_CONFIGMAP_PRECOMPOSE) && + precompose) + flags |= GIT_REFERENCE_FORMAT__PRECOMPOSE_UNICODE; + + if (!validate) + flags |= GIT_REFERENCE_FORMAT__VALIDATION_DISABLE; + + return git_reference_normalize_name(out, GIT_REFNAME_MAX, name, flags); +} + +int git_reference_lookup_resolved( + git_reference **ref_out, + git_repository *repo, + const char *name, + int max_nesting) +{ + git_refname_t normalized; + git_refdb *refdb; + int error = 0; + + GIT_ASSERT_ARG(ref_out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + if ((error = reference_normalize_for_repo(normalized, repo, name, true)) < 0 || + (error = git_repository_refdb__weakptr(&refdb, repo)) < 0 || + (error = git_refdb_resolve(ref_out, refdb, normalized, max_nesting)) < 0) + return error; + + /* + * The resolved reference may be a symbolic reference in case its + * target doesn't exist. If the user asked us to resolve (e.g. + * `max_nesting != 0`), then we need to return an error in case we got + * a symbolic reference back. + */ + if (max_nesting && git_reference_type(*ref_out) == GIT_REFERENCE_SYMBOLIC) { + git_reference_free(*ref_out); + *ref_out = NULL; + return GIT_ENOTFOUND; + } + + return 0; +} + +int git_reference_dwim(git_reference **out, git_repository *repo, const char *refname) +{ + int error = 0, i, valid; + bool fallbackmode = true, foundvalid = false; + git_reference *ref; + git_str refnamebuf = GIT_STR_INIT, name = GIT_STR_INIT; + + static const char *formatters[] = { + "%s", + GIT_REFS_DIR "%s", + GIT_REFS_TAGS_DIR "%s", + GIT_REFS_HEADS_DIR "%s", + GIT_REFS_REMOTES_DIR "%s", + GIT_REFS_REMOTES_DIR "%s/" GIT_HEAD_FILE, + NULL + }; + + if (*refname) + git_str_puts(&name, refname); + else { + git_str_puts(&name, GIT_HEAD_FILE); + fallbackmode = false; + } + + for (i = 0; formatters[i] && (fallbackmode || i == 0); i++) { + + git_str_clear(&refnamebuf); + + if ((error = git_str_printf(&refnamebuf, formatters[i], git_str_cstr(&name))) < 0 || + (error = git_reference_name_is_valid(&valid, git_str_cstr(&refnamebuf))) < 0) + goto cleanup; + + if (!valid) { + error = GIT_EINVALIDSPEC; + continue; + } + foundvalid = true; + + error = git_reference_lookup_resolved(&ref, repo, git_str_cstr(&refnamebuf), -1); + + if (!error) { + *out = ref; + error = 0; + goto cleanup; + } + + if (error != GIT_ENOTFOUND) + goto cleanup; + } + +cleanup: + if (error && !foundvalid) { + /* never found a valid reference name */ + git_error_set(GIT_ERROR_REFERENCE, + "could not use '%s' as valid reference name", git_str_cstr(&name)); + } + + if (error == GIT_ENOTFOUND) + git_error_set(GIT_ERROR_REFERENCE, "no reference found for shorthand '%s'", refname); + + git_str_dispose(&name); + git_str_dispose(&refnamebuf); + return error; +} + +/** + * Getters + */ +git_reference_t git_reference_type(const git_reference *ref) +{ + GIT_ASSERT_ARG(ref); + return ref->type; +} + +const char *git_reference_name(const git_reference *ref) +{ + GIT_ASSERT_ARG_WITH_RETVAL(ref, NULL); + return ref->name; +} + +git_repository *git_reference_owner(const git_reference *ref) +{ + GIT_ASSERT_ARG_WITH_RETVAL(ref, NULL); + return ref->db->repo; +} + +const git_oid *git_reference_target(const git_reference *ref) +{ + GIT_ASSERT_ARG_WITH_RETVAL(ref, NULL); + + if (ref->type != GIT_REFERENCE_DIRECT) + return NULL; + + return &ref->target.oid; +} + +const git_oid *git_reference_target_peel(const git_reference *ref) +{ + GIT_ASSERT_ARG_WITH_RETVAL(ref, NULL); + + if (ref->type != GIT_REFERENCE_DIRECT || git_oid_is_zero(&ref->peel)) + return NULL; + + return &ref->peel; +} + +const char *git_reference_symbolic_target(const git_reference *ref) +{ + GIT_ASSERT_ARG_WITH_RETVAL(ref, NULL); + + if (ref->type != GIT_REFERENCE_SYMBOLIC) + return NULL; + + return ref->target.symbolic; +} + +static int reference__create( + git_reference **ref_out, + git_repository *repo, + const char *name, + const git_oid *oid, + const char *symbolic, + int force, + const git_signature *signature, + const char *log_message, + const git_oid *old_id, + const char *old_target) +{ + git_refname_t normalized; + git_refdb *refdb; + git_reference *ref = NULL; + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + GIT_ASSERT_ARG(symbolic || signature); + + if (ref_out) + *ref_out = NULL; + + error = reference_normalize_for_repo(normalized, repo, name, true); + if (error < 0) + return error; + + error = git_repository_refdb__weakptr(&refdb, repo); + if (error < 0) + return error; + + if (oid != NULL) { + GIT_ASSERT(symbolic == NULL); + + if (!git_object__is_valid(repo, oid, GIT_OBJECT_ANY)) { + git_error_set(GIT_ERROR_REFERENCE, + "target OID for the reference doesn't exist on the repository"); + return -1; + } + + ref = git_reference__alloc(normalized, oid, NULL); + } else { + git_refname_t normalized_target; + + error = reference_normalize_for_repo(normalized_target, repo, + symbolic, git_reference__enable_symbolic_ref_target_validation); + + if (error < 0) + return error; + + ref = git_reference__alloc_symbolic(normalized, normalized_target); + } + + GIT_ERROR_CHECK_ALLOC(ref); + + if ((error = git_refdb_write(refdb, ref, force, signature, log_message, old_id, old_target)) < 0) { + git_reference_free(ref); + return error; + } + + if (ref_out == NULL) + git_reference_free(ref); + else + *ref_out = ref; + + return 0; +} + +static int refs_configured_ident(git_signature **out, const git_repository *repo) +{ + if (repo->ident_name && repo->ident_email) + return git_signature_now(out, repo->ident_name, repo->ident_email); + + /* if not configured let us fall-through to the next method */ + return -1; +} + +int git_reference__log_signature(git_signature **out, git_repository *repo) +{ + int error; + git_signature *who; + + if(((error = refs_configured_ident(&who, repo)) < 0) && + ((error = git_signature_default(&who, repo)) < 0) && + ((error = git_signature_now(&who, "unknown", "unknown")) < 0)) + return error; + + *out = who; + return 0; +} + +int git_reference_create_matching( + git_reference **ref_out, + git_repository *repo, + const char *name, + const git_oid *id, + int force, + const git_oid *old_id, + const char *log_message) + +{ + int error; + git_signature *who = NULL; + + GIT_ASSERT_ARG(id); + + if ((error = git_reference__log_signature(&who, repo)) < 0) + return error; + + error = reference__create( + ref_out, repo, name, id, NULL, force, who, log_message, old_id, NULL); + + git_signature_free(who); + return error; +} + +int git_reference_create( + git_reference **ref_out, + git_repository *repo, + const char *name, + const git_oid *id, + int force, + const char *log_message) +{ + return git_reference_create_matching(ref_out, repo, name, id, force, NULL, log_message); +} + +int git_reference_symbolic_create_matching( + git_reference **ref_out, + git_repository *repo, + const char *name, + const char *target, + int force, + const char *old_target, + const char *log_message) +{ + int error; + git_signature *who = NULL; + + GIT_ASSERT_ARG(target); + + if ((error = git_reference__log_signature(&who, repo)) < 0) + return error; + + error = reference__create( + ref_out, repo, name, NULL, target, force, who, log_message, NULL, old_target); + + git_signature_free(who); + return error; +} + +int git_reference_symbolic_create( + git_reference **ref_out, + git_repository *repo, + const char *name, + const char *target, + int force, + const char *log_message) +{ + return git_reference_symbolic_create_matching(ref_out, repo, name, target, force, NULL, log_message); +} + +static int ensure_is_an_updatable_direct_reference(git_reference *ref) +{ + if (ref->type == GIT_REFERENCE_DIRECT) + return 0; + + git_error_set(GIT_ERROR_REFERENCE, "cannot set OID on symbolic reference"); + return -1; +} + +int git_reference_set_target( + git_reference **out, + git_reference *ref, + const git_oid *id, + const char *log_message) +{ + int error; + git_repository *repo; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(ref); + GIT_ASSERT_ARG(id); + + repo = ref->db->repo; + + if ((error = ensure_is_an_updatable_direct_reference(ref)) < 0) + return error; + + return git_reference_create_matching(out, repo, ref->name, id, 1, &ref->target.oid, log_message); +} + +static int ensure_is_an_updatable_symbolic_reference(git_reference *ref) +{ + if (ref->type == GIT_REFERENCE_SYMBOLIC) + return 0; + + git_error_set(GIT_ERROR_REFERENCE, "cannot set symbolic target on a direct reference"); + return -1; +} + +int git_reference_symbolic_set_target( + git_reference **out, + git_reference *ref, + const char *target, + const char *log_message) +{ + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(ref); + GIT_ASSERT_ARG(target); + + if ((error = ensure_is_an_updatable_symbolic_reference(ref)) < 0) + return error; + + return git_reference_symbolic_create_matching( + out, ref->db->repo, ref->name, target, 1, ref->target.symbolic, log_message); +} + +typedef struct { + const char *old_name; + git_refname_t new_name; +} refs_update_head_payload; + +static int refs_update_head(git_repository *worktree, void *_payload) +{ + refs_update_head_payload *payload = (refs_update_head_payload *)_payload; + git_reference *head = NULL, *updated = NULL; + int error; + + if ((error = git_reference_lookup(&head, worktree, GIT_HEAD_FILE)) < 0) + goto out; + + if (git_reference_type(head) != GIT_REFERENCE_SYMBOLIC || + git__strcmp(git_reference_symbolic_target(head), payload->old_name) != 0) + goto out; + + /* Update HEAD if it was pointing to the reference being renamed */ + if ((error = git_reference_symbolic_set_target(&updated, head, payload->new_name, NULL)) < 0) { + git_error_set(GIT_ERROR_REFERENCE, "failed to update HEAD after renaming reference"); + goto out; + } + +out: + git_reference_free(updated); + git_reference_free(head); + return error; +} + +int git_reference_rename( + git_reference **out, + git_reference *ref, + const char *new_name, + int force, + const char *log_message) +{ + refs_update_head_payload payload; + git_signature *signature = NULL; + git_repository *repo; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(ref); + + repo = git_reference_owner(ref); + + if ((error = git_reference__log_signature(&signature, repo)) < 0 || + (error = reference_normalize_for_repo(payload.new_name, repo, new_name, true)) < 0 || + (error = git_refdb_rename(out, ref->db, ref->name, payload.new_name, force, signature, log_message)) < 0) + goto out; + + payload.old_name = ref->name; + + /* We may have to update any HEAD that was pointing to the renamed reference. */ + if ((error = git_repository_foreach_worktree(repo, refs_update_head, &payload)) < 0) + goto out; + +out: + git_signature_free(signature); + return error; +} + +int git_reference_resolve(git_reference **ref_out, const git_reference *ref) +{ + switch (git_reference_type(ref)) { + case GIT_REFERENCE_DIRECT: + return git_reference_lookup(ref_out, ref->db->repo, ref->name); + + case GIT_REFERENCE_SYMBOLIC: + return git_reference_lookup_resolved(ref_out, ref->db->repo, ref->target.symbolic, -1); + + default: + git_error_set(GIT_ERROR_REFERENCE, "invalid reference"); + return -1; + } +} + +int git_reference_foreach( + git_repository *repo, + git_reference_foreach_cb callback, + void *payload) +{ + git_reference_iterator *iter; + git_reference *ref; + int error; + + if ((error = git_reference_iterator_new(&iter, repo)) < 0) + return error; + + while (!(error = git_reference_next(&ref, iter))) { + if ((error = callback(ref, payload)) != 0) { + git_error_set_after_callback(error); + break; + } + } + + if (error == GIT_ITEROVER) + error = 0; + + git_reference_iterator_free(iter); + return error; +} + +int git_reference_foreach_name( + git_repository *repo, + git_reference_foreach_name_cb callback, + void *payload) +{ + git_reference_iterator *iter; + const char *refname; + int error; + + if ((error = git_reference_iterator_new(&iter, repo)) < 0) + return error; + + while (!(error = git_reference_next_name(&refname, iter))) { + if ((error = callback(refname, payload)) != 0) { + git_error_set_after_callback(error); + break; + } + } + + if (error == GIT_ITEROVER) + error = 0; + + git_reference_iterator_free(iter); + return error; +} + +int git_reference_foreach_glob( + git_repository *repo, + const char *glob, + git_reference_foreach_name_cb callback, + void *payload) +{ + git_reference_iterator *iter; + const char *refname; + int error; + + if ((error = git_reference_iterator_glob_new(&iter, repo, glob)) < 0) + return error; + + while (!(error = git_reference_next_name(&refname, iter))) { + if ((error = callback(refname, payload)) != 0) { + git_error_set_after_callback(error); + break; + } + } + + if (error == GIT_ITEROVER) + error = 0; + + git_reference_iterator_free(iter); + return error; +} + +int git_reference_iterator_new(git_reference_iterator **out, git_repository *repo) +{ + git_refdb *refdb; + + if (git_repository_refdb__weakptr(&refdb, repo) < 0) + return -1; + + return git_refdb_iterator(out, refdb, NULL); +} + +int git_reference_iterator_glob_new( + git_reference_iterator **out, git_repository *repo, const char *glob) +{ + git_refdb *refdb; + + if (git_repository_refdb__weakptr(&refdb, repo) < 0) + return -1; + + return git_refdb_iterator(out, refdb, glob); +} + +int git_reference_next(git_reference **out, git_reference_iterator *iter) +{ + return git_refdb_iterator_next(out, iter); +} + +int git_reference_next_name(const char **out, git_reference_iterator *iter) +{ + return git_refdb_iterator_next_name(out, iter); +} + +void git_reference_iterator_free(git_reference_iterator *iter) +{ + if (iter == NULL) + return; + + git_refdb_iterator_free(iter); +} + +static int cb__reflist_add(const char *ref, void *data) +{ + char *name = git__strdup(ref); + GIT_ERROR_CHECK_ALLOC(name); + return git_vector_insert((git_vector *)data, name); +} + +int git_reference_list( + git_strarray *array, + git_repository *repo) +{ + git_vector ref_list; + + GIT_ASSERT_ARG(array); + GIT_ASSERT_ARG(repo); + + array->strings = NULL; + array->count = 0; + + if (git_vector_init(&ref_list, 8, NULL) < 0) + return -1; + + if (git_reference_foreach_name( + repo, &cb__reflist_add, (void *)&ref_list) < 0) { + git_vector_free(&ref_list); + return -1; + } + + array->strings = (char **)git_vector_detach(&array->count, NULL, &ref_list); + + return 0; +} + +static int is_valid_ref_char(char ch) +{ + if ((unsigned) ch <= ' ') + return 0; + + switch (ch) { + case '~': + case '^': + case ':': + case '\\': + case '?': + case '[': + return 0; + default: + return 1; + } +} + +static int ensure_segment_validity(const char *name, char may_contain_glob) +{ + const char *current = name; + char prev = '\0'; + const int lock_len = (int)strlen(GIT_FILELOCK_EXTENSION); + int segment_len; + + if (*current == '.') + return -1; /* Refname starts with "." */ + + for (current = name; ; current++) { + if (*current == '\0' || *current == '/') + break; + + if (!is_valid_ref_char(*current)) + return -1; /* Illegal character in refname */ + + if (prev == '.' && *current == '.') + return -1; /* Refname contains ".." */ + + if (prev == '@' && *current == '{') + return -1; /* Refname contains "@{" */ + + if (*current == '*') { + if (!may_contain_glob) + return -1; + may_contain_glob = 0; + } + + prev = *current; + } + + segment_len = (int)(current - name); + + /* A refname component can not end with ".lock" */ + if (segment_len >= lock_len && + !memcmp(current - lock_len, GIT_FILELOCK_EXTENSION, lock_len)) + return -1; + + return segment_len; +} + +static bool is_all_caps_and_underscore(const char *name, size_t len) +{ + size_t i; + char c; + + GIT_ASSERT_ARG(name); + GIT_ASSERT_ARG(len > 0); + + for (i = 0; i < len; i++) + { + c = name[i]; + if ((c < 'A' || c > 'Z') && c != '_') + return false; + } + + if (*name == '_' || name[len - 1] == '_') + return false; + + return true; +} + +/* Inspired from https://github.com/git/git/blob/f06d47e7e0d9db709ee204ed13a8a7486149f494/refs.c#L36-100 */ +int git_reference__normalize_name( + git_str *buf, + const char *name, + unsigned int flags) +{ + const char *current; + int segment_len, segments_count = 0, error = GIT_EINVALIDSPEC; + unsigned int process_flags; + bool normalize = (buf != NULL); + bool validate = (flags & GIT_REFERENCE_FORMAT__VALIDATION_DISABLE) == 0; + +#ifdef GIT_USE_ICONV + git_fs_path_iconv_t ic = GIT_PATH_ICONV_INIT; +#endif + + GIT_ASSERT_ARG(name); + + process_flags = flags; + current = (char *)name; + + if (validate && *current == '/') + goto cleanup; + + if (normalize) + git_str_clear(buf); + +#ifdef GIT_USE_ICONV + if ((flags & GIT_REFERENCE_FORMAT__PRECOMPOSE_UNICODE) != 0) { + size_t namelen = strlen(current); + if ((error = git_fs_path_iconv_init_precompose(&ic)) < 0 || + (error = git_fs_path_iconv(&ic, ¤t, &namelen)) < 0) + goto cleanup; + error = GIT_EINVALIDSPEC; + } +#endif + + if (!validate) { + git_str_sets(buf, current); + + error = git_str_oom(buf) ? -1 : 0; + goto cleanup; + } + + while (true) { + char may_contain_glob = process_flags & GIT_REFERENCE_FORMAT_REFSPEC_PATTERN; + + segment_len = ensure_segment_validity(current, may_contain_glob); + if (segment_len < 0) + goto cleanup; + + if (segment_len > 0) { + /* + * There may only be one glob in a pattern, thus we reset + * the pattern-flag in case the current segment has one. + */ + if (memchr(current, '*', segment_len)) + process_flags &= ~GIT_REFERENCE_FORMAT_REFSPEC_PATTERN; + + if (normalize) { + size_t cur_len = git_str_len(buf); + + git_str_joinpath(buf, git_str_cstr(buf), current); + git_str_truncate(buf, + cur_len + segment_len + (segments_count ? 1 : 0)); + + if (git_str_oom(buf)) { + error = -1; + goto cleanup; + } + } + + segments_count++; + } + + /* No empty segment is allowed when not normalizing */ + if (segment_len == 0 && !normalize) + goto cleanup; + + if (current[segment_len] == '\0') + break; + + current += segment_len + 1; + } + + /* A refname can not be empty */ + if (segment_len == 0 && segments_count == 0) + goto cleanup; + + /* A refname can not end with "." */ + if (current[segment_len - 1] == '.') + goto cleanup; + + /* A refname can not end with "/" */ + if (current[segment_len - 1] == '/') + goto cleanup; + + if ((segments_count == 1 ) && !(flags & GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL)) + goto cleanup; + + if ((segments_count == 1 ) && + !(flags & GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND) && + !(is_all_caps_and_underscore(name, (size_t)segment_len) || + ((flags & GIT_REFERENCE_FORMAT_REFSPEC_PATTERN) && !strcmp("*", name)))) + goto cleanup; + + if ((segments_count > 1) + && (is_all_caps_and_underscore(name, strchr(name, '/') - name))) + goto cleanup; + + error = 0; + +cleanup: + if (error == GIT_EINVALIDSPEC) + git_error_set( + GIT_ERROR_REFERENCE, + "the given reference name '%s' is not valid", name); + + if (error && normalize) + git_str_dispose(buf); + +#ifdef GIT_USE_ICONV + git_fs_path_iconv_clear(&ic); +#endif + + return error; +} + +int git_reference_normalize_name( + char *buffer_out, + size_t buffer_size, + const char *name, + unsigned int flags) +{ + git_str buf = GIT_STR_INIT; + int error; + + if ((error = git_reference__normalize_name(&buf, name, flags)) < 0) + goto cleanup; + + if (git_str_len(&buf) > buffer_size - 1) { + git_error_set( + GIT_ERROR_REFERENCE, + "the provided buffer is too short to hold the normalization of '%s'", name); + error = GIT_EBUFS; + goto cleanup; + } + + if ((error = git_str_copy_cstr(buffer_out, buffer_size, &buf)) < 0) + goto cleanup; + + error = 0; + +cleanup: + git_str_dispose(&buf); + return error; +} + +#define GIT_REFERENCE_TYPEMASK (GIT_REFERENCE_DIRECT | GIT_REFERENCE_SYMBOLIC) + +int git_reference_cmp( + const git_reference *ref1, + const git_reference *ref2) +{ + git_reference_t type1, type2; + + GIT_ASSERT_ARG(ref1); + GIT_ASSERT_ARG(ref2); + + type1 = git_reference_type(ref1); + type2 = git_reference_type(ref2); + + /* let's put symbolic refs before OIDs */ + if (type1 != type2) + return (type1 == GIT_REFERENCE_SYMBOLIC) ? -1 : 1; + + if (type1 == GIT_REFERENCE_SYMBOLIC) + return strcmp(ref1->target.symbolic, ref2->target.symbolic); + + return git_oid__cmp(&ref1->target.oid, &ref2->target.oid); +} + +/* + * Starting with the reference given by `ref_name`, follows symbolic + * references until a direct reference is found and updated the OID + * on that direct reference to `oid`. + */ +int git_reference__update_terminal( + git_repository *repo, + const char *ref_name, + const git_oid *oid, + const git_signature *sig, + const char *log_message) +{ + git_reference *ref = NULL, *ref2 = NULL; + git_signature *who = NULL; + git_refdb *refdb = NULL; + const git_signature *to_use; + int error = 0; + + if (!sig && (error = git_reference__log_signature(&who, repo)) < 0) + goto out; + + to_use = sig ? sig : who; + + if ((error = git_repository_refdb__weakptr(&refdb, repo)) < 0) + goto out; + + if ((error = git_refdb_resolve(&ref, refdb, ref_name, -1)) < 0) { + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = reference__create(&ref2, repo, ref_name, oid, NULL, 0, to_use, + log_message, NULL, NULL); + } + goto out; + } + + /* In case the resolved reference is symbolic, then it's a dangling symref. */ + if (git_reference_type(ref) == GIT_REFERENCE_SYMBOLIC) { + error = reference__create(&ref2, repo, ref->target.symbolic, oid, NULL, 0, to_use, + log_message, NULL, NULL); + } else { + error = reference__create(&ref2, repo, ref->name, oid, NULL, 1, to_use, + log_message, &ref->target.oid, NULL); + } + +out: + git_reference_free(ref2); + git_reference_free(ref); + git_signature_free(who); + return error; +} + +static const char *commit_type(const git_commit *commit) +{ + unsigned int count = git_commit_parentcount(commit); + + if (count >= 2) + return " (merge)"; + else if (count == 0) + return " (initial)"; + else + return ""; +} + +int git_reference__update_for_commit( + git_repository *repo, + git_reference *ref, + const char *ref_name, + const git_oid *id, + const char *operation) +{ + git_reference *ref_new = NULL; + git_commit *commit = NULL; + git_str reflog_msg = GIT_STR_INIT; + const git_signature *who; + int error; + + if ((error = git_commit_lookup(&commit, repo, id)) < 0 || + (error = git_str_printf(&reflog_msg, "%s%s: %s", + operation ? operation : "commit", + commit_type(commit), + git_commit_summary(commit))) < 0) + goto done; + + who = git_commit_committer(commit); + + if (ref) { + if ((error = ensure_is_an_updatable_direct_reference(ref)) < 0) + return error; + + error = reference__create(&ref_new, repo, ref->name, id, NULL, 1, who, + git_str_cstr(&reflog_msg), &ref->target.oid, NULL); + } + else + error = git_reference__update_terminal( + repo, ref_name, id, who, git_str_cstr(&reflog_msg)); + +done: + git_reference_free(ref_new); + git_str_dispose(&reflog_msg); + git_commit_free(commit); + return error; +} + +int git_reference_has_log(git_repository *repo, const char *refname) +{ + int error; + git_refdb *refdb; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(refname); + + if ((error = git_repository_refdb__weakptr(&refdb, repo)) < 0) + return error; + + return git_refdb_has_log(refdb, refname); +} + +int git_reference_ensure_log(git_repository *repo, const char *refname) +{ + int error; + git_refdb *refdb; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(refname); + + if ((error = git_repository_refdb__weakptr(&refdb, repo)) < 0) + return error; + + return git_refdb_ensure_log(refdb, refname); +} + +int git_reference__is_branch(const char *ref_name) +{ + return git__prefixcmp(ref_name, GIT_REFS_HEADS_DIR) == 0; +} + +int git_reference_is_branch(const git_reference *ref) +{ + GIT_ASSERT_ARG(ref); + return git_reference__is_branch(ref->name); +} + +int git_reference__is_remote(const char *ref_name) +{ + return git__prefixcmp(ref_name, GIT_REFS_REMOTES_DIR) == 0; +} + +int git_reference_is_remote(const git_reference *ref) +{ + GIT_ASSERT_ARG(ref); + return git_reference__is_remote(ref->name); +} + +int git_reference__is_tag(const char *ref_name) +{ + return git__prefixcmp(ref_name, GIT_REFS_TAGS_DIR) == 0; +} + +int git_reference_is_tag(const git_reference *ref) +{ + GIT_ASSERT_ARG(ref); + return git_reference__is_tag(ref->name); +} + +int git_reference__is_note(const char *ref_name) +{ + return git__prefixcmp(ref_name, GIT_REFS_NOTES_DIR) == 0; +} + +int git_reference_is_note(const git_reference *ref) +{ + GIT_ASSERT_ARG(ref); + return git_reference__is_note(ref->name); +} + +static int peel_error(int error, const git_reference *ref, const char *msg) +{ + git_error_set( + GIT_ERROR_INVALID, + "the reference '%s' cannot be peeled - %s", git_reference_name(ref), msg); + return error; +} + +int git_reference_peel( + git_object **peeled, + const git_reference *ref, + git_object_t target_type) +{ + const git_reference *resolved = NULL; + git_reference *allocated = NULL; + git_object *target = NULL; + int error; + + GIT_ASSERT_ARG(ref); + + if (ref->type == GIT_REFERENCE_DIRECT) { + resolved = ref; + } else { + if ((error = git_reference_resolve(&allocated, ref)) < 0) + return peel_error(error, ref, "Cannot resolve reference"); + + resolved = allocated; + } + + /* + * If we try to peel an object to a tag, we cannot use + * the fully peeled object, as that will always resolve + * to a commit. So we only want to use the peeled value + * if it is not zero and the target is not a tag. + */ + if (target_type != GIT_OBJECT_TAG && !git_oid_is_zero(&resolved->peel)) { + error = git_object_lookup(&target, + git_reference_owner(ref), &resolved->peel, GIT_OBJECT_ANY); + } else { + error = git_object_lookup(&target, + git_reference_owner(ref), &resolved->target.oid, GIT_OBJECT_ANY); + } + + if (error < 0) { + peel_error(error, ref, "Cannot retrieve reference target"); + goto cleanup; + } + + if (target_type == GIT_OBJECT_ANY && git_object_type(target) != GIT_OBJECT_TAG) + error = git_object_dup(peeled, target); + else + error = git_object_peel(peeled, target, target_type); + +cleanup: + git_object_free(target); + git_reference_free(allocated); + + return error; +} + +int git_reference__name_is_valid( + int *valid, + const char *refname, + unsigned int flags) +{ + int error; + + GIT_ASSERT(valid && refname); + + *valid = 0; + + error = git_reference__normalize_name(NULL, refname, flags); + + if (!error) + *valid = 1; + else if (error == GIT_EINVALIDSPEC) + error = 0; + + return error; +} + +int git_reference_name_is_valid(int *valid, const char *refname) +{ + return git_reference__name_is_valid(valid, refname, GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL); +} + +const char *git_reference__shorthand(const char *name) +{ + if (!git__prefixcmp(name, GIT_REFS_HEADS_DIR)) + return name + strlen(GIT_REFS_HEADS_DIR); + else if (!git__prefixcmp(name, GIT_REFS_TAGS_DIR)) + return name + strlen(GIT_REFS_TAGS_DIR); + else if (!git__prefixcmp(name, GIT_REFS_REMOTES_DIR)) + return name + strlen(GIT_REFS_REMOTES_DIR); + else if (!git__prefixcmp(name, GIT_REFS_DIR)) + return name + strlen(GIT_REFS_DIR); + + /* No shorthands are available, so just return the name. */ + return name; +} + +const char *git_reference_shorthand(const git_reference *ref) +{ + return git_reference__shorthand(ref->name); +} + +int git_reference__is_unborn_head(bool *unborn, const git_reference *ref, git_repository *repo) +{ + int error; + git_reference *tmp_ref; + + GIT_ASSERT_ARG(unborn); + GIT_ASSERT_ARG(ref); + GIT_ASSERT_ARG(repo); + + if (ref->type == GIT_REFERENCE_DIRECT) { + *unborn = 0; + return 0; + } + + error = git_reference_lookup_resolved(&tmp_ref, repo, ref->name, -1); + git_reference_free(tmp_ref); + + if (error != 0 && error != GIT_ENOTFOUND) + return error; + else if (error == GIT_ENOTFOUND && git__strcmp(ref->name, GIT_HEAD_FILE) == 0) + *unborn = true; + else + *unborn = false; + + return 0; +} + +/* Deprecated functions */ + +#ifndef GIT_DEPRECATE_HARD + +int git_reference_is_valid_name(const char *refname) +{ + int valid = 0; + + git_reference__name_is_valid(&valid, refname, GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL); + + return valid; +} + +#endif diff --git a/src/libgit2/refs.h b/src/libgit2/refs.h new file mode 100644 index 0000000..cb888bf --- /dev/null +++ b/src/libgit2/refs.h @@ -0,0 +1,130 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_refs_h__ +#define INCLUDE_refs_h__ + +#include "common.h" + +#include "git2/oid.h" +#include "git2/refs.h" +#include "git2/refdb.h" +#include "strmap.h" +#include "str.h" +#include "oid.h" + +extern bool git_reference__enable_symbolic_ref_target_validation; + +#define GIT_REFS_DIR "refs/" +#define GIT_REFS_HEADS_DIR GIT_REFS_DIR "heads/" +#define GIT_REFS_TAGS_DIR GIT_REFS_DIR "tags/" +#define GIT_REFS_REMOTES_DIR GIT_REFS_DIR "remotes/" +#define GIT_REFS_NOTES_DIR GIT_REFS_DIR "notes/" +#define GIT_REFS_DIR_MODE 0777 +#define GIT_REFS_FILE_MODE 0666 + +#define GIT_RENAMED_REF_FILE GIT_REFS_DIR "RENAMED-REF" + +#define GIT_SYMREF "ref: " +#define GIT_PACKEDREFS_FILE "packed-refs" +#define GIT_PACKEDREFS_HEADER "# pack-refs with: peeled fully-peeled sorted " +#define GIT_PACKEDREFS_FILE_MODE 0666 + +#define GIT_HEAD_FILE "HEAD" +#define GIT_ORIG_HEAD_FILE "ORIG_HEAD" +#define GIT_FETCH_HEAD_FILE "FETCH_HEAD" +#define GIT_MERGE_HEAD_FILE "MERGE_HEAD" +#define GIT_REVERT_HEAD_FILE "REVERT_HEAD" +#define GIT_CHERRYPICK_HEAD_FILE "CHERRY_PICK_HEAD" +#define GIT_BISECT_LOG_FILE "BISECT_LOG" +#define GIT_REBASE_MERGE_DIR "rebase-merge/" +#define GIT_REBASE_MERGE_INTERACTIVE_FILE GIT_REBASE_MERGE_DIR "interactive" +#define GIT_REBASE_APPLY_DIR "rebase-apply/" +#define GIT_REBASE_APPLY_REBASING_FILE GIT_REBASE_APPLY_DIR "rebasing" +#define GIT_REBASE_APPLY_APPLYING_FILE GIT_REBASE_APPLY_DIR "applying" + +#define GIT_SEQUENCER_DIR "sequencer/" +#define GIT_SEQUENCER_HEAD_FILE GIT_SEQUENCER_DIR "head" +#define GIT_SEQUENCER_OPTIONS_FILE GIT_SEQUENCER_DIR "options" +#define GIT_SEQUENCER_TODO_FILE GIT_SEQUENCER_DIR "todo" + +#define GIT_STASH_FILE "stash" +#define GIT_REFS_STASH_FILE GIT_REFS_DIR GIT_STASH_FILE + +#define GIT_REFERENCE_FORMAT__PRECOMPOSE_UNICODE (1u << 16) +#define GIT_REFERENCE_FORMAT__VALIDATION_DISABLE (1u << 15) + +#define GIT_REFNAME_MAX 1024 + +typedef char git_refname_t[GIT_REFNAME_MAX]; + +struct git_reference { + git_refdb *db; + git_reference_t type; + + union { + git_oid oid; + char *symbolic; + } target; + + git_oid peel; + char name[GIT_FLEX_ARRAY]; +}; + +/** + * Reallocate the reference with a new name + * + * Note that this is a dangerous operation, as on success, all existing + * pointers to the old reference will now be dangling. Only call this on objects + * you control, possibly using `git_reference_dup`. + */ +git_reference *git_reference__realloc(git_reference **ptr_to_ref, const char *name); + +int git_reference__normalize_name(git_str *buf, const char *name, unsigned int flags); +int git_reference__update_terminal(git_repository *repo, const char *ref_name, const git_oid *oid, const git_signature *sig, const char *log_message); +int git_reference__name_is_valid(int *valid, const char *name, unsigned int flags); +int git_reference__is_branch(const char *ref_name); +int git_reference__is_remote(const char *ref_name); +int git_reference__is_tag(const char *ref_name); +int git_reference__is_note(const char *ref_name); +const char *git_reference__shorthand(const char *name); + +/** + * Lookup a reference by name and try to resolve to an OID. + * + * You can control how many dereferences this will attempt to resolve the + * reference with the `max_deref` parameter, or pass -1 to use a sane + * default. If you pass 0 for `max_deref`, this will not attempt to resolve + * the reference. For any value of `max_deref` other than 0, not + * successfully resolving the reference will be reported as an error. + + * The generated reference must be freed by the user. + * + * @param reference_out Pointer to the looked-up reference + * @param repo The repository to look up the reference + * @param name The long name for the reference (e.g. HEAD, ref/heads/master, refs/tags/v0.1.0, ...) + * @param max_deref Maximum number of dereferences to make of symbolic refs, 0 means simple lookup, < 0 means use default reasonable value + * @return 0 on success or < 0 on error; not being able to resolve the reference is an error unless 0 was passed for max_deref + */ +int git_reference_lookup_resolved( + git_reference **reference_out, + git_repository *repo, + const char *name, + int max_deref); + +int git_reference__log_signature(git_signature **out, git_repository *repo); + +/** Update a reference after a commit. */ +int git_reference__update_for_commit( + git_repository *repo, + git_reference *ref, + const char *ref_name, + const git_oid *id, + const char *operation); + +int git_reference__is_unborn_head(bool *unborn, const git_reference *ref, git_repository *repo); + +#endif diff --git a/src/libgit2/refspec.c b/src/libgit2/refspec.c new file mode 100644 index 0000000..f0a0c2b --- /dev/null +++ b/src/libgit2/refspec.c @@ -0,0 +1,420 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "refspec.h" + +#include "buf.h" +#include "refs.h" +#include "util.h" +#include "vector.h" +#include "wildmatch.h" + +int git_refspec__parse(git_refspec *refspec, const char *input, bool is_fetch) +{ + /* Ported from https://github.com/git/git/blob/f06d47e7e0d9db709ee204ed13a8a7486149f494/remote.c#L518-636 */ + + size_t llen; + int is_glob = 0; + const char *lhs, *rhs; + int valid = 0; + unsigned int flags; + + GIT_ASSERT_ARG(refspec); + GIT_ASSERT_ARG(input); + + memset(refspec, 0x0, sizeof(git_refspec)); + refspec->push = !is_fetch; + + lhs = input; + if (*lhs == '+') { + refspec->force = 1; + lhs++; + } + + rhs = strrchr(lhs, ':'); + + /* + * Before going on, special case ":" (or "+:") as a refspec + * for matching refs. + */ + if (!is_fetch && rhs == lhs && rhs[1] == '\0') { + refspec->matching = 1; + refspec->string = git__strdup(input); + GIT_ERROR_CHECK_ALLOC(refspec->string); + refspec->src = git__strdup(""); + GIT_ERROR_CHECK_ALLOC(refspec->src); + refspec->dst = git__strdup(""); + GIT_ERROR_CHECK_ALLOC(refspec->dst); + return 0; + } + + if (rhs) { + size_t rlen = strlen(++rhs); + if (rlen || !is_fetch) { + is_glob = (1 <= rlen && strchr(rhs, '*')); + refspec->dst = git__strndup(rhs, rlen); + } + } + + llen = (rhs ? (size_t)(rhs - lhs - 1) : strlen(lhs)); + if (1 <= llen && memchr(lhs, '*', llen)) { + if ((rhs && !is_glob) || (!rhs && is_fetch)) + goto invalid; + is_glob = 1; + } else if (rhs && is_glob) + goto invalid; + + refspec->pattern = is_glob; + refspec->src = git__strndup(lhs, llen); + flags = GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL | + GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND | + (is_glob ? GIT_REFERENCE_FORMAT_REFSPEC_PATTERN : 0); + + if (is_fetch) { + /* + * LHS + * - empty is allowed; it means HEAD. + * - otherwise it must be a valid looking ref. + */ + if (!*refspec->src) + ; /* empty is ok */ + else if (git_reference__name_is_valid(&valid, refspec->src, flags) < 0) + goto on_error; + else if (!valid) + goto invalid; + + /* + * RHS + * - missing is ok, and is same as empty. + * - empty is ok; it means not to store. + * - otherwise it must be a valid looking ref. + */ + if (!refspec->dst) + ; /* ok */ + else if (!*refspec->dst) + ; /* ok */ + else if (git_reference__name_is_valid(&valid, refspec->dst, flags) < 0) + goto on_error; + else if (!valid) + goto invalid; + } else { + /* + * LHS + * - empty is allowed; it means delete. + * - when wildcarded, it must be a valid looking ref. + * - otherwise, it must be an extended SHA-1, but + * there is no existing way to validate this. + */ + if (!*refspec->src) + ; /* empty is ok */ + else if (is_glob) { + if (git_reference__name_is_valid(&valid, refspec->src, flags) < 0) + goto on_error; + else if (!valid) + goto invalid; + } + else { + ; /* anything goes, for now */ + } + + /* + * RHS + * - missing is allowed, but LHS then must be a + * valid looking ref. + * - empty is not allowed. + * - otherwise it must be a valid looking ref. + */ + if (!refspec->dst) { + if (git_reference__name_is_valid(&valid, refspec->src, flags) < 0) + goto on_error; + else if (!valid) + goto invalid; + } else if (!*refspec->dst) { + goto invalid; + } else { + if (git_reference__name_is_valid(&valid, refspec->dst, flags) < 0) + goto on_error; + else if (!valid) + goto invalid; + } + + /* if the RHS is empty, then it's a copy of the LHS */ + if (!refspec->dst) { + refspec->dst = git__strdup(refspec->src); + GIT_ERROR_CHECK_ALLOC(refspec->dst); + } + } + + refspec->string = git__strdup(input); + GIT_ERROR_CHECK_ALLOC(refspec->string); + + return 0; + +invalid: + git_error_set(GIT_ERROR_INVALID, + "'%s' is not a valid refspec.", input); + git_refspec__dispose(refspec); + return GIT_EINVALIDSPEC; + +on_error: + git_refspec__dispose(refspec); + return -1; +} + +void git_refspec__dispose(git_refspec *refspec) +{ + if (refspec == NULL) + return; + + git__free(refspec->src); + git__free(refspec->dst); + git__free(refspec->string); + + memset(refspec, 0x0, sizeof(git_refspec)); +} + +int git_refspec_parse(git_refspec **out_refspec, const char *input, int is_fetch) +{ + git_refspec *refspec; + GIT_ASSERT_ARG(out_refspec); + GIT_ASSERT_ARG(input); + + *out_refspec = NULL; + + refspec = git__malloc(sizeof(git_refspec)); + GIT_ERROR_CHECK_ALLOC(refspec); + + if (git_refspec__parse(refspec, input, !!is_fetch) != 0) { + git__free(refspec); + return -1; + } + + *out_refspec = refspec; + return 0; +} + +void git_refspec_free(git_refspec *refspec) +{ + git_refspec__dispose(refspec); + git__free(refspec); +} + +const char *git_refspec_src(const git_refspec *refspec) +{ + return refspec == NULL ? NULL : refspec->src; +} + +const char *git_refspec_dst(const git_refspec *refspec) +{ + return refspec == NULL ? NULL : refspec->dst; +} + +const char *git_refspec_string(const git_refspec *refspec) +{ + return refspec == NULL ? NULL : refspec->string; +} + +int git_refspec_force(const git_refspec *refspec) +{ + GIT_ASSERT_ARG(refspec); + + return refspec->force; +} + +int git_refspec_src_matches(const git_refspec *refspec, const char *refname) +{ + if (refspec == NULL || refspec->src == NULL) + return false; + + return (wildmatch(refspec->src, refname, 0) == 0); +} + +int git_refspec_dst_matches(const git_refspec *refspec, const char *refname) +{ + if (refspec == NULL || refspec->dst == NULL) + return false; + + return (wildmatch(refspec->dst, refname, 0) == 0); +} + +static int refspec_transform( + git_str *out, const char *from, const char *to, const char *name) +{ + const char *from_star, *to_star; + size_t replacement_len, star_offset; + + git_str_clear(out); + + /* + * There are two parts to each side of a refspec, the bit + * before the star and the bit after it. The star can be in + * the middle of the pattern, so we need to look at each bit + * individually. + */ + from_star = strchr(from, '*'); + to_star = strchr(to, '*'); + + GIT_ASSERT(from_star && to_star); + + /* star offset, both in 'from' and in 'name' */ + star_offset = from_star - from; + + /* the first half is copied over */ + git_str_put(out, to, to_star - to); + + /* + * Copy over the name, but exclude the trailing part in "from" starting + * after the glob + */ + replacement_len = strlen(name + star_offset) - strlen(from_star + 1); + git_str_put(out, name + star_offset, replacement_len); + + return git_str_puts(out, to_star + 1); +} + +int git_refspec_transform(git_buf *out, const git_refspec *spec, const char *name) +{ + GIT_BUF_WRAP_PRIVATE(out, git_refspec__transform, spec, name); +} + +int git_refspec__transform(git_str *out, const git_refspec *spec, const char *name) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(spec); + GIT_ASSERT_ARG(name); + + if (!git_refspec_src_matches(spec, name)) { + git_error_set(GIT_ERROR_INVALID, "ref '%s' doesn't match the source", name); + return -1; + } + + if (!spec->pattern) + return git_str_puts(out, spec->dst ? spec->dst : ""); + + return refspec_transform(out, spec->src, spec->dst, name); +} + +int git_refspec_rtransform(git_buf *out, const git_refspec *spec, const char *name) +{ + GIT_BUF_WRAP_PRIVATE(out, git_refspec__rtransform, spec, name); +} + +int git_refspec__rtransform(git_str *out, const git_refspec *spec, const char *name) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(spec); + GIT_ASSERT_ARG(name); + + if (!git_refspec_dst_matches(spec, name)) { + git_error_set(GIT_ERROR_INVALID, "ref '%s' doesn't match the destination", name); + return -1; + } + + if (!spec->pattern) + return git_str_puts(out, spec->src); + + return refspec_transform(out, spec->dst, spec->src, name); +} + +int git_refspec__serialize(git_str *out, const git_refspec *refspec) +{ + if (refspec->force) + git_str_putc(out, '+'); + + git_str_printf(out, "%s:%s", + refspec->src != NULL ? refspec->src : "", + refspec->dst != NULL ? refspec->dst : ""); + + return git_str_oom(out) == false; +} + +int git_refspec_is_wildcard(const git_refspec *spec) +{ + GIT_ASSERT_ARG(spec); + GIT_ASSERT_ARG(spec->src); + + return (spec->src[strlen(spec->src) - 1] == '*'); +} + +git_direction git_refspec_direction(const git_refspec *spec) +{ + GIT_ASSERT_ARG(spec); + + return spec->push; +} + +int git_refspec__dwim_one(git_vector *out, git_refspec *spec, git_vector *refs) +{ + git_str buf = GIT_STR_INIT; + size_t j, pos; + git_remote_head key; + git_refspec *cur; + + const char *formatters[] = { + GIT_REFS_DIR "%s", + GIT_REFS_TAGS_DIR "%s", + GIT_REFS_HEADS_DIR "%s", + NULL + }; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(spec); + GIT_ASSERT_ARG(refs); + + cur = git__calloc(1, sizeof(git_refspec)); + GIT_ERROR_CHECK_ALLOC(cur); + + cur->force = spec->force; + cur->push = spec->push; + cur->pattern = spec->pattern; + cur->matching = spec->matching; + cur->string = git__strdup(spec->string); + + /* shorthand on the lhs */ + if (git__prefixcmp(spec->src, GIT_REFS_DIR)) { + for (j = 0; formatters[j]; j++) { + git_str_clear(&buf); + git_str_printf(&buf, formatters[j], spec->src); + GIT_ERROR_CHECK_ALLOC_STR(&buf); + + key.name = (char *) git_str_cstr(&buf); + if (!git_vector_search(&pos, refs, &key)) { + /* we found something to match the shorthand, set src to that */ + cur->src = git_str_detach(&buf); + } + } + } + + /* No shorthands found, copy over the name */ + if (cur->src == NULL && spec->src != NULL) { + cur->src = git__strdup(spec->src); + GIT_ERROR_CHECK_ALLOC(cur->src); + } + + if (spec->dst && git__prefixcmp(spec->dst, GIT_REFS_DIR)) { + /* if it starts with "remotes" then we just prepend "refs/" */ + if (!git__prefixcmp(spec->dst, "remotes/")) { + git_str_puts(&buf, GIT_REFS_DIR); + } else { + git_str_puts(&buf, GIT_REFS_HEADS_DIR); + } + + git_str_puts(&buf, spec->dst); + GIT_ERROR_CHECK_ALLOC_STR(&buf); + + cur->dst = git_str_detach(&buf); + } + + git_str_dispose(&buf); + + if (cur->dst == NULL && spec->dst != NULL) { + cur->dst = git__strdup(spec->dst); + GIT_ERROR_CHECK_ALLOC(cur->dst); + } + + return git_vector_insert(out, cur); +} diff --git a/src/libgit2/refspec.h b/src/libgit2/refspec.h new file mode 100644 index 0000000..bf4f7fc --- /dev/null +++ b/src/libgit2/refspec.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_refspec_h__ +#define INCLUDE_refspec_h__ + +#include "common.h" + +#include "git2/refspec.h" +#include "str.h" +#include "vector.h" + +struct git_refspec { + char *string; + char *src; + char *dst; + unsigned int force :1, + push : 1, + pattern :1, + matching :1; +}; + +#define GIT_REFSPEC_TAGS "refs/tags/*:refs/tags/*" + +int git_refspec__transform(git_str *out, const git_refspec *spec, const char *name); +int git_refspec__rtransform(git_str *out, const git_refspec *spec, const char *name); + +int git_refspec__parse( + struct git_refspec *refspec, + const char *str, + bool is_fetch); + +void git_refspec__dispose(git_refspec *refspec); + +int git_refspec__serialize(git_str *out, const git_refspec *refspec); + +/** + * Determines if a refspec is a wildcard refspec. + * + * @param spec the refspec + * @return 1 if the refspec is a wildcard, 0 otherwise + */ +int git_refspec_is_wildcard(const git_refspec *spec); + +/** + * DWIM `spec` with `refs` existing on the remote, append the dwim'ed + * result in `out`. + */ +int git_refspec__dwim_one(git_vector *out, git_refspec *spec, git_vector *refs); + +#endif diff --git a/src/libgit2/remote.c b/src/libgit2/remote.c new file mode 100644 index 0000000..fee2a7f --- /dev/null +++ b/src/libgit2/remote.c @@ -0,0 +1,3097 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "remote.h" + +#include "buf.h" +#include "branch.h" +#include "config.h" +#include "repository.h" +#include "fetch.h" +#include "refs.h" +#include "refspec.h" +#include "fetchhead.h" +#include "push.h" +#include "proxy.h" +#include "strarray.h" + +#include "git2/config.h" +#include "git2/types.h" +#include "git2/oid.h" +#include "git2/net.h" +#include "transports/smart.h" + +#define CONFIG_URL_FMT "remote.%s.url" +#define CONFIG_PUSHURL_FMT "remote.%s.pushurl" +#define CONFIG_FETCH_FMT "remote.%s.fetch" +#define CONFIG_PUSH_FMT "remote.%s.push" +#define CONFIG_TAGOPT_FMT "remote.%s.tagopt" + +static int dwim_refspecs(git_vector *out, git_vector *refspecs, git_vector *refs); +static int lookup_remote_prune_config(git_remote *remote, git_config *config, const char *name); +static int apply_insteadof(char **out, git_config *config, const char *url, int direction, bool use_default_if_empty); + +static int add_refspec_to(git_vector *vector, const char *string, bool is_fetch) +{ + git_refspec *spec; + + spec = git__calloc(1, sizeof(git_refspec)); + GIT_ERROR_CHECK_ALLOC(spec); + + if (git_refspec__parse(spec, string, is_fetch) < 0) { + git__free(spec); + return -1; + } + + spec->push = !is_fetch; + if (git_vector_insert(vector, spec) < 0) { + git_refspec__dispose(spec); + git__free(spec); + return -1; + } + + return 0; +} + +static int add_refspec(git_remote *remote, const char *string, bool is_fetch) +{ + return add_refspec_to(&remote->refspecs, string, is_fetch); +} + +static int download_tags_value(git_remote *remote, git_config *cfg) +{ + git_config_entry *ce; + git_str buf = GIT_STR_INIT; + int error; + + if (git_str_printf(&buf, "remote.%s.tagopt", remote->name) < 0) + return -1; + + error = git_config__lookup_entry(&ce, cfg, git_str_cstr(&buf), false); + git_str_dispose(&buf); + + if (!error && ce && ce->value) { + if (!strcmp(ce->value, "--no-tags")) + remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_NONE; + else if (!strcmp(ce->value, "--tags")) + remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_ALL; + } + + git_config_entry_free(ce); + return error; +} + +static int ensure_remote_name_is_valid(const char *name) +{ + int valid, error; + + error = git_remote_name_is_valid(&valid, name); + + if (!error && !valid) { + git_error_set( + GIT_ERROR_CONFIG, + "'%s' is not a valid remote name.", name ? name : "(null)"); + error = GIT_EINVALIDSPEC; + } + + return error; +} + +static int write_add_refspec(git_repository *repo, const char *name, const char *refspec, bool fetch) +{ + git_config *cfg; + git_str var = GIT_STR_INIT; + git_refspec spec; + const char *fmt; + int error; + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; + + fmt = fetch ? CONFIG_FETCH_FMT : CONFIG_PUSH_FMT; + + if ((error = ensure_remote_name_is_valid(name)) < 0) + return error; + + if ((error = git_refspec__parse(&spec, refspec, fetch)) < 0) + return error; + + git_refspec__dispose(&spec); + + if ((error = git_str_printf(&var, fmt, name)) < 0) + return error; + + /* + * "$^" is an unmatchable regexp: it will not match anything at all, so + * all values will be considered new and we will not replace any + * present value. + */ + if ((error = git_config_set_multivar(cfg, var.ptr, "$^", refspec)) < 0) { + goto cleanup; + } + +cleanup: + git_str_dispose(&var); + return 0; +} + +static int canonicalize_url(git_str *out, const char *in) +{ + if (in == NULL || strlen(in) == 0) { + git_error_set(GIT_ERROR_INVALID, "cannot set empty URL"); + return GIT_EINVALIDSPEC; + } + +#ifdef GIT_WIN32 + /* Given a UNC path like \\server\path, we need to convert this + * to //server/path for compatibility with core git. + */ + if (in[0] == '\\' && in[1] == '\\' && + (git__isalpha(in[2]) || git__isdigit(in[2]))) { + const char *c; + for (c = in; *c; c++) + git_str_putc(out, *c == '\\' ? '/' : *c); + + return git_str_oom(out) ? -1 : 0; + } +#endif + + return git_str_puts(out, in); +} + +static int default_fetchspec_for_name(git_str *buf, const char *name) +{ + if (git_str_printf(buf, "+refs/heads/*:refs/remotes/%s/*", name) < 0) + return -1; + + return 0; +} + +static int ensure_remote_doesnot_exist(git_repository *repo, const char *name) +{ + int error; + git_remote *remote; + + error = git_remote_lookup(&remote, repo, name); + + if (error == GIT_ENOTFOUND) + return 0; + + if (error < 0) + return error; + + git_remote_free(remote); + + git_error_set(GIT_ERROR_CONFIG, "remote '%s' already exists", name); + + return GIT_EEXISTS; +} + +int git_remote_create_options_init(git_remote_create_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_remote_create_options, GIT_REMOTE_CREATE_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_remote_create_init_options(git_remote_create_options *opts, unsigned int version) +{ + return git_remote_create_options_init(opts, version); +} +#endif + +int git_remote_create_with_opts(git_remote **out, const char *url, const git_remote_create_options *opts) +{ + git_remote *remote = NULL; + git_config *config_ro = NULL, *config_rw; + git_str canonical_url = GIT_STR_INIT; + git_str var = GIT_STR_INIT; + git_str specbuf = GIT_STR_INIT; + const git_remote_create_options dummy_opts = GIT_REMOTE_CREATE_OPTIONS_INIT; + int error = -1; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(url); + + if (!opts) { + opts = &dummy_opts; + } + + GIT_ERROR_CHECK_VERSION(opts, GIT_REMOTE_CREATE_OPTIONS_VERSION, "git_remote_create_options"); + + if (opts->name != NULL) { + if ((error = ensure_remote_name_is_valid(opts->name)) < 0) + return error; + + if (opts->repository && + (error = ensure_remote_doesnot_exist(opts->repository, opts->name)) < 0) + return error; + } + + if (opts->repository) { + if ((error = git_repository_config_snapshot(&config_ro, opts->repository)) < 0) + goto on_error; + } + + remote = git__calloc(1, sizeof(git_remote)); + GIT_ERROR_CHECK_ALLOC(remote); + + remote->repo = opts->repository; + + if ((error = git_vector_init(&remote->refs, 8, NULL)) < 0 || + (error = canonicalize_url(&canonical_url, url)) < 0) + goto on_error; + + if (opts->repository && !(opts->flags & GIT_REMOTE_CREATE_SKIP_INSTEADOF)) { + if ((error = apply_insteadof(&remote->url, config_ro, canonical_url.ptr, GIT_DIRECTION_FETCH, true)) < 0 || + (error = apply_insteadof(&remote->pushurl, config_ro, canonical_url.ptr, GIT_DIRECTION_PUSH, false)) < 0) + goto on_error; + } else { + remote->url = git__strdup(canonical_url.ptr); + GIT_ERROR_CHECK_ALLOC(remote->url); + } + + if (opts->name != NULL) { + remote->name = git__strdup(opts->name); + GIT_ERROR_CHECK_ALLOC(remote->name); + + if (opts->repository && + ((error = git_str_printf(&var, CONFIG_URL_FMT, opts->name)) < 0 || + (error = git_repository_config__weakptr(&config_rw, opts->repository)) < 0 || + (error = git_config_set_string(config_rw, var.ptr, canonical_url.ptr)) < 0)) + goto on_error; + } + + if (opts->fetchspec != NULL || + (opts->name && !(opts->flags & GIT_REMOTE_CREATE_SKIP_DEFAULT_FETCHSPEC))) { + const char *fetch = NULL; + if (opts->fetchspec) { + fetch = opts->fetchspec; + } else { + if ((error = default_fetchspec_for_name(&specbuf, opts->name)) < 0) + goto on_error; + + fetch = git_str_cstr(&specbuf); + } + + if ((error = add_refspec(remote, fetch, true)) < 0) + goto on_error; + + /* only write for named remotes with a repository */ + if (opts->repository && opts->name && + ((error = write_add_refspec(opts->repository, opts->name, fetch, true)) < 0 || + (error = lookup_remote_prune_config(remote, config_ro, opts->name)) < 0)) + goto on_error; + + /* Move the data over to where the matching functions can find them */ + if ((error = dwim_refspecs(&remote->active_refspecs, &remote->refspecs, &remote->refs)) < 0) + goto on_error; + } + + /* A remote without a name doesn't download tags */ + if (!opts->name) + remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_NONE; + else + remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_AUTO; + + + git_str_dispose(&var); + + *out = remote; + error = 0; + +on_error: + if (error) + git_remote_free(remote); + + git_config_free(config_ro); + git_str_dispose(&specbuf); + git_str_dispose(&canonical_url); + git_str_dispose(&var); + return error; +} + +int git_remote_create(git_remote **out, git_repository *repo, const char *name, const char *url) +{ + git_str buf = GIT_STR_INIT; + int error; + git_remote_create_options opts = GIT_REMOTE_CREATE_OPTIONS_INIT; + + /* Those 2 tests are duplicated here because of backward-compatibility */ + if ((error = ensure_remote_name_is_valid(name)) < 0) + return error; + + if (canonicalize_url(&buf, url) < 0) + return GIT_ERROR; + + git_str_clear(&buf); + + opts.repository = repo; + opts.name = name; + + error = git_remote_create_with_opts(out, url, &opts); + + git_str_dispose(&buf); + + return error; +} + +int git_remote_create_with_fetchspec(git_remote **out, git_repository *repo, const char *name, const char *url, const char *fetch) +{ + int error; + git_remote_create_options opts = GIT_REMOTE_CREATE_OPTIONS_INIT; + + if ((error = ensure_remote_name_is_valid(name)) < 0) + return error; + + opts.repository = repo; + opts.name = name; + opts.fetchspec = fetch; + opts.flags = GIT_REMOTE_CREATE_SKIP_DEFAULT_FETCHSPEC; + + return git_remote_create_with_opts(out, url, &opts); +} + +int git_remote_create_anonymous(git_remote **out, git_repository *repo, const char *url) +{ + git_remote_create_options opts = GIT_REMOTE_CREATE_OPTIONS_INIT; + + opts.repository = repo; + + return git_remote_create_with_opts(out, url, &opts); +} + +int git_remote_create_detached(git_remote **out, const char *url) +{ + return git_remote_create_with_opts(out, url, NULL); +} + +int git_remote_dup(git_remote **dest, git_remote *source) +{ + size_t i; + int error = 0; + git_refspec *spec; + git_remote *remote = git__calloc(1, sizeof(git_remote)); + GIT_ERROR_CHECK_ALLOC(remote); + + if (source->name != NULL) { + remote->name = git__strdup(source->name); + GIT_ERROR_CHECK_ALLOC(remote->name); + } + + if (source->url != NULL) { + remote->url = git__strdup(source->url); + GIT_ERROR_CHECK_ALLOC(remote->url); + } + + if (source->pushurl != NULL) { + remote->pushurl = git__strdup(source->pushurl); + GIT_ERROR_CHECK_ALLOC(remote->pushurl); + } + + remote->repo = source->repo; + remote->download_tags = source->download_tags; + remote->prune_refs = source->prune_refs; + + if (git_vector_init(&remote->refs, 32, NULL) < 0 || + git_vector_init(&remote->refspecs, 2, NULL) < 0 || + git_vector_init(&remote->active_refspecs, 2, NULL) < 0) { + error = -1; + goto cleanup; + } + + git_vector_foreach(&source->refspecs, i, spec) { + if ((error = add_refspec(remote, spec->string, !spec->push)) < 0) + goto cleanup; + } + + *dest = remote; + +cleanup: + + if (error < 0) + git__free(remote); + + return error; +} + +struct refspec_cb_data { + git_remote *remote; + int fetch; +}; + +static int refspec_cb(const git_config_entry *entry, void *payload) +{ + struct refspec_cb_data *data = (struct refspec_cb_data *)payload; + return add_refspec(data->remote, entry->value, data->fetch); +} + +static int get_optional_config( + bool *found, git_config *config, git_str *buf, + git_config_foreach_cb cb, void *payload) +{ + int error = 0; + const char *key = git_str_cstr(buf); + + if (git_str_oom(buf)) + return -1; + + if (cb != NULL) + error = git_config_get_multivar_foreach(config, key, NULL, cb, payload); + else + error = git_config_get_string(payload, config, key); + + if (found) + *found = !error; + + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = 0; + } + + return error; +} + +int git_remote_lookup(git_remote **out, git_repository *repo, const char *name) +{ + git_remote *remote = NULL; + git_str buf = GIT_STR_INIT; + const char *val; + int error = 0; + git_config *config; + struct refspec_cb_data data = { NULL }; + bool optional_setting_found = false, found; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + if ((error = ensure_remote_name_is_valid(name)) < 0) + return error; + + if ((error = git_repository_config_snapshot(&config, repo)) < 0) + return error; + + remote = git__calloc(1, sizeof(git_remote)); + GIT_ERROR_CHECK_ALLOC(remote); + + remote->name = git__strdup(name); + GIT_ERROR_CHECK_ALLOC(remote->name); + + if (git_vector_init(&remote->refs, 32, NULL) < 0 || + git_vector_init(&remote->refspecs, 2, NULL) < 0 || + git_vector_init(&remote->passive_refspecs, 2, NULL) < 0 || + git_vector_init(&remote->active_refspecs, 2, NULL) < 0) { + error = -1; + goto cleanup; + } + + if ((error = git_str_printf(&buf, "remote.%s.url", name)) < 0) + goto cleanup; + + if ((error = get_optional_config(&found, config, &buf, NULL, (void *)&val)) < 0) + goto cleanup; + + optional_setting_found |= found; + + remote->repo = repo; + remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_AUTO; + + if (found && strlen(val) > 0) { + if ((error = apply_insteadof(&remote->url, config, val, GIT_DIRECTION_FETCH, true)) < 0 || + (error = apply_insteadof(&remote->pushurl, config, val, GIT_DIRECTION_PUSH, false)) < 0) + goto cleanup; + } + + val = NULL; + git_str_clear(&buf); + git_str_printf(&buf, "remote.%s.pushurl", name); + + if ((error = get_optional_config(&found, config, &buf, NULL, (void *)&val)) < 0) + goto cleanup; + + optional_setting_found |= found; + + if (!optional_setting_found) { + error = GIT_ENOTFOUND; + git_error_set(GIT_ERROR_CONFIG, "remote '%s' does not exist", name); + goto cleanup; + } + + if (found && strlen(val) > 0) { + if (remote->pushurl) + git__free(remote->pushurl); + + if ((error = apply_insteadof(&remote->pushurl, config, val, GIT_DIRECTION_FETCH, true)) < 0) + goto cleanup; + } + + data.remote = remote; + data.fetch = true; + + git_str_clear(&buf); + git_str_printf(&buf, "remote.%s.fetch", name); + + if ((error = get_optional_config(NULL, config, &buf, refspec_cb, &data)) < 0) + goto cleanup; + + data.fetch = false; + git_str_clear(&buf); + git_str_printf(&buf, "remote.%s.push", name); + + if ((error = get_optional_config(NULL, config, &buf, refspec_cb, &data)) < 0) + goto cleanup; + + if ((error = download_tags_value(remote, config)) < 0) + goto cleanup; + + if ((error = lookup_remote_prune_config(remote, config, name)) < 0) + goto cleanup; + + /* Move the data over to where the matching functions can find them */ + if ((error = dwim_refspecs(&remote->active_refspecs, &remote->refspecs, &remote->refs)) < 0) + goto cleanup; + + *out = remote; + +cleanup: + git_config_free(config); + git_str_dispose(&buf); + + if (error < 0) + git_remote_free(remote); + + return error; +} + +static int lookup_remote_prune_config(git_remote *remote, git_config *config, const char *name) +{ + git_str buf = GIT_STR_INIT; + int error = 0; + + git_str_printf(&buf, "remote.%s.prune", name); + + if ((error = git_config_get_bool(&remote->prune_refs, config, git_str_cstr(&buf))) < 0) { + if (error == GIT_ENOTFOUND) { + git_error_clear(); + + if ((error = git_config_get_bool(&remote->prune_refs, config, "fetch.prune")) < 0) { + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = 0; + } + } + } + } + + git_str_dispose(&buf); + return error; +} + +const char *git_remote_name(const git_remote *remote) +{ + GIT_ASSERT_ARG_WITH_RETVAL(remote, NULL); + return remote->name; +} + +git_repository *git_remote_owner(const git_remote *remote) +{ + GIT_ASSERT_ARG_WITH_RETVAL(remote, NULL); + return remote->repo; +} + +const char *git_remote_url(const git_remote *remote) +{ + GIT_ASSERT_ARG_WITH_RETVAL(remote, NULL); + return remote->url; +} + +int git_remote_set_instance_url(git_remote *remote, const char *url) +{ + char *tmp; + + GIT_ASSERT_ARG(remote); + GIT_ASSERT_ARG(url); + + if ((tmp = git__strdup(url)) == NULL) + return -1; + + git__free(remote->url); + remote->url = tmp; + + return 0; +} + +static int set_url(git_repository *repo, const char *remote, const char *pattern, const char *url) +{ + git_config *cfg; + git_str buf = GIT_STR_INIT, canonical_url = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(remote); + + if ((error = ensure_remote_name_is_valid(remote)) < 0) + return error; + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; + + if ((error = git_str_printf(&buf, pattern, remote)) < 0) + return error; + + if (url) { + if ((error = canonicalize_url(&canonical_url, url)) < 0) + goto cleanup; + + error = git_config_set_string(cfg, buf.ptr, url); + } else { + error = git_config_delete_entry(cfg, buf.ptr); + } + +cleanup: + git_str_dispose(&canonical_url); + git_str_dispose(&buf); + + return error; +} + +int git_remote_set_url(git_repository *repo, const char *remote, const char *url) +{ + return set_url(repo, remote, CONFIG_URL_FMT, url); +} + +const char *git_remote_pushurl(const git_remote *remote) +{ + GIT_ASSERT_ARG_WITH_RETVAL(remote, NULL); + return remote->pushurl; +} + +int git_remote_set_instance_pushurl(git_remote *remote, const char *url) +{ + char *tmp; + + GIT_ASSERT_ARG(remote); + GIT_ASSERT_ARG(url); + + if ((tmp = git__strdup(url)) == NULL) + return -1; + + git__free(remote->pushurl); + remote->pushurl = tmp; + + return 0; +} + +int git_remote_set_pushurl(git_repository *repo, const char *remote, const char *url) +{ + return set_url(repo, remote, CONFIG_PUSHURL_FMT, url); +} + +static int resolve_url( + git_str *resolved_url, + const char *url, + int direction, + const git_remote_callbacks *callbacks) +{ +#ifdef GIT_DEPRECATE_HARD + GIT_UNUSED(direction); + GIT_UNUSED(callbacks); +#else + git_buf buf = GIT_BUF_INIT; + int error; + + if (callbacks && callbacks->resolve_url) { + error = callbacks->resolve_url(&buf, url, direction, callbacks->payload); + + if (error != GIT_PASSTHROUGH) { + git_error_set_after_callback_function(error, "git_resolve_url_cb"); + + git_str_set(resolved_url, buf.ptr, buf.size); + git_buf_dispose(&buf); + + return error; + } + } +#endif + + return git_str_sets(resolved_url, url); +} + +int git_remote__urlfordirection( + git_str *url_out, + struct git_remote *remote, + int direction, + const git_remote_callbacks *callbacks) +{ + const char *url = NULL; + + GIT_ASSERT_ARG(remote); + GIT_ASSERT_ARG(direction == GIT_DIRECTION_FETCH || direction == GIT_DIRECTION_PUSH); + + if (callbacks && callbacks->remote_ready) { + int status = callbacks->remote_ready(remote, direction, callbacks->payload); + + if (status != 0 && status != GIT_PASSTHROUGH) { + git_error_set_after_callback_function(status, "git_remote_ready_cb"); + return status; + } + } + + if (direction == GIT_DIRECTION_FETCH) + url = remote->url; + else if (direction == GIT_DIRECTION_PUSH) + url = remote->pushurl ? remote->pushurl : remote->url; + + if (!url) { + git_error_set(GIT_ERROR_INVALID, + "malformed remote '%s' - missing %s URL", + remote->name ? remote->name : "(anonymous)", + direction == GIT_DIRECTION_FETCH ? "fetch" : "push"); + return GIT_EINVALID; + } + + return resolve_url(url_out, url, direction, callbacks); +} + +int git_remote_connect_options_init( + git_remote_connect_options *opts, + unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_remote_connect_options, GIT_REMOTE_CONNECT_OPTIONS_INIT); + return 0; +} + +int git_remote_connect_options_dup( + git_remote_connect_options *dst, + const git_remote_connect_options *src) +{ + memcpy(dst, src, sizeof(git_remote_connect_options)); + + if (git_proxy_options_dup(&dst->proxy_opts, &src->proxy_opts) < 0 || + git_strarray_copy(&dst->custom_headers, &src->custom_headers) < 0) + return -1; + + return 0; +} + +void git_remote_connect_options_dispose(git_remote_connect_options *opts) +{ + if (!opts) + return; + + git_strarray_dispose(&opts->custom_headers); + git_proxy_options_dispose(&opts->proxy_opts); +} + +static size_t http_header_name_length(const char *http_header) +{ + const char *colon = strchr(http_header, ':'); + if (!colon) + return 0; + return colon - http_header; +} + +static bool is_malformed_http_header(const char *http_header) +{ + const char *c; + size_t name_len; + + /* Disallow \r and \n */ + if ((c = strchr(http_header, '\r')) != NULL) + return true; + if ((c = strchr(http_header, '\n')) != NULL) + return true; + + /* Require a header name followed by : */ + if ((name_len = http_header_name_length(http_header)) < 1) + return true; + + return false; +} + +static char *forbidden_custom_headers[] = { + "User-Agent", + "Host", + "Accept", + "Content-Type", + "Transfer-Encoding", + "Content-Length", +}; + +static bool is_forbidden_custom_header(const char *custom_header) +{ + unsigned long i; + size_t name_len = http_header_name_length(custom_header); + + /* Disallow headers that we set */ + for (i = 0; i < ARRAY_SIZE(forbidden_custom_headers); i++) + if (strncmp(forbidden_custom_headers[i], custom_header, name_len) == 0) + return true; + + return false; +} + +static int validate_custom_headers(const git_strarray *custom_headers) +{ + size_t i; + + if (!custom_headers) + return 0; + + for (i = 0; i < custom_headers->count; i++) { + if (is_malformed_http_header(custom_headers->strings[i])) { + git_error_set(GIT_ERROR_INVALID, "custom HTTP header '%s' is malformed", custom_headers->strings[i]); + return -1; + } + + if (is_forbidden_custom_header(custom_headers->strings[i])) { + git_error_set(GIT_ERROR_INVALID, "custom HTTP header '%s' is already set by libgit2", custom_headers->strings[i]); + return -1; + } + } + + return 0; +} + +static int lookup_redirect_config( + git_remote_redirect_t *out, + git_repository *repo) +{ + git_config *config; + const char *value; + int bool_value, error = 0; + + if (!repo) { + *out = GIT_REMOTE_REDIRECT_INITIAL; + return 0; + } + + if ((error = git_repository_config_snapshot(&config, repo)) < 0) + goto done; + + if ((error = git_config_get_string(&value, config, "http.followRedirects")) < 0) { + if (error == GIT_ENOTFOUND) { + *out = GIT_REMOTE_REDIRECT_INITIAL; + error = 0; + } + + goto done; + } + + if (git_config_parse_bool(&bool_value, value) == 0) { + *out = bool_value ? GIT_REMOTE_REDIRECT_ALL : + GIT_REMOTE_REDIRECT_NONE; + } else if (strcasecmp(value, "initial") == 0) { + *out = GIT_REMOTE_REDIRECT_INITIAL; + } else { + git_error_set(GIT_ERROR_CONFIG, "invalid configuration setting '%s' for 'http.followRedirects'", value); + error = -1; + } + +done: + git_config_free(config); + return error; +} + +int git_remote_connect_options_normalize( + git_remote_connect_options *dst, + git_repository *repo, + const git_remote_connect_options *src) +{ + git_remote_connect_options_dispose(dst); + git_remote_connect_options_init(dst, GIT_REMOTE_CONNECT_OPTIONS_VERSION); + + if (src) { + GIT_ERROR_CHECK_VERSION(src, GIT_REMOTE_CONNECT_OPTIONS_VERSION, "git_remote_connect_options"); + GIT_ERROR_CHECK_VERSION(&src->callbacks, GIT_REMOTE_CALLBACKS_VERSION, "git_remote_callbacks"); + GIT_ERROR_CHECK_VERSION(&src->proxy_opts, GIT_PROXY_OPTIONS_VERSION, "git_proxy_options"); + + if (validate_custom_headers(&src->custom_headers) < 0 || + git_remote_connect_options_dup(dst, src) < 0) + return -1; + } + + if (dst->follow_redirects == 0) { + if (lookup_redirect_config(&dst->follow_redirects, repo) < 0) + return -1; + } + + return 0; +} + +int git_remote_connect_ext( + git_remote *remote, + git_direction direction, + const git_remote_connect_options *given_opts) +{ + git_remote_connect_options opts = GIT_REMOTE_CONNECT_OPTIONS_INIT; + git_str url = GIT_STR_INIT; + git_transport *t; + int error; + + GIT_ASSERT_ARG(remote); + + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_remote_connect_options)); + + GIT_ERROR_CHECK_VERSION(&opts.callbacks, GIT_REMOTE_CALLBACKS_VERSION, "git_remote_callbacks"); + GIT_ERROR_CHECK_VERSION(&opts.proxy_opts, GIT_PROXY_OPTIONS_VERSION, "git_proxy_options"); + + t = remote->transport; + + if ((error = git_remote__urlfordirection(&url, remote, direction, &opts.callbacks)) < 0) + goto on_error; + + /* If we don't have a transport object yet, and the caller specified a + * custom transport factory, use that */ + if (!t && opts.callbacks.transport && + (error = opts.callbacks.transport(&t, remote, opts.callbacks.payload)) < 0) + goto on_error; + + /* If we still don't have a transport, then use the global + * transport registrations which map URI schemes to transport factories */ + if (!t && (error = git_transport_new(&t, remote, url.ptr)) < 0) + goto on_error; + + if ((error = t->connect(t, url.ptr, direction, &opts)) != 0) + goto on_error; + + remote->transport = t; + + git_str_dispose(&url); + + return 0; + +on_error: + if (t) + t->free(t); + + git_str_dispose(&url); + + if (t == remote->transport) + remote->transport = NULL; + + return error; +} + +int git_remote_connect( + git_remote *remote, + git_direction direction, + const git_remote_callbacks *callbacks, + const git_proxy_options *proxy, + const git_strarray *custom_headers) +{ + git_remote_connect_options opts = GIT_REMOTE_CONNECT_OPTIONS_INIT; + + if (callbacks) + memcpy(&opts.callbacks, callbacks, sizeof(git_remote_callbacks)); + + if (proxy) + memcpy(&opts.proxy_opts, proxy, sizeof(git_proxy_options)); + + if (custom_headers) + memcpy(&opts.custom_headers, custom_headers, sizeof(git_strarray)); + + return git_remote_connect_ext(remote, direction, &opts); +} + +int git_remote_ls(const git_remote_head ***out, size_t *size, git_remote *remote) +{ + GIT_ASSERT_ARG(remote); + + if (!remote->transport) { + git_error_set(GIT_ERROR_NET, "this remote has never connected"); + return -1; + } + + return remote->transport->ls(out, size, remote->transport); +} + +int git_remote_capabilities(unsigned int *out, git_remote *remote) +{ + GIT_ASSERT_ARG(remote); + + *out = 0; + + if (!remote->transport) { + git_error_set(GIT_ERROR_NET, "this remote has never connected"); + return -1; + } + + return remote->transport->capabilities(out, remote->transport); +} + +int git_remote_oid_type(git_oid_t *out, git_remote *remote) +{ + GIT_ASSERT_ARG(remote); + + if (!remote->transport) { + git_error_set(GIT_ERROR_NET, "this remote has never connected"); + *out = 0; + return -1; + } + +#ifdef GIT_EXPERIMENTAL_SHA256 + return remote->transport->oid_type(out, remote->transport); +#else + *out = GIT_OID_SHA1; + return 0; +#endif +} + +static int lookup_config(char **out, git_config *cfg, const char *name) +{ + git_config_entry *ce = NULL; + int error; + + if ((error = git_config__lookup_entry(&ce, cfg, name, false)) < 0) + return error; + + if (ce && ce->value) { + *out = git__strdup(ce->value); + GIT_ERROR_CHECK_ALLOC(*out); + } else { + error = GIT_ENOTFOUND; + } + + git_config_entry_free(ce); + return error; +} + +static void url_config_trim(git_net_url *url) +{ + size_t len = strlen(url->path); + + if (url->path[len - 1] == '/') { + len--; + } else { + while (len && url->path[len - 1] != '/') + len--; + } + + url->path[len] = '\0'; +} + +static int http_proxy_config(char **out, git_remote *remote, git_net_url *url) +{ + git_config *cfg = NULL; + git_str buf = GIT_STR_INIT; + git_net_url lookup_url = GIT_NET_URL_INIT; + int error; + + if ((error = git_net_url_dup(&lookup_url, url)) < 0) + goto done; + + if (remote->repo) { + if ((error = git_repository_config(&cfg, remote->repo)) < 0) + goto done; + } else { + if ((error = git_config_open_default(&cfg)) < 0) + goto done; + } + + /* remote..proxy config setting */ + if (remote->name && remote->name[0]) { + git_str_clear(&buf); + + if ((error = git_str_printf(&buf, "remote.%s.proxy", remote->name)) < 0 || + (error = lookup_config(out, cfg, buf.ptr)) != GIT_ENOTFOUND) + goto done; + } + + while (true) { + git_str_clear(&buf); + + if ((error = git_str_puts(&buf, "http.")) < 0 || + (error = git_net_url_fmt(&buf, &lookup_url)) < 0 || + (error = git_str_puts(&buf, ".proxy")) < 0 || + (error = lookup_config(out, cfg, buf.ptr)) != GIT_ENOTFOUND) + goto done; + + if (! lookup_url.path[0]) + break; + + url_config_trim(&lookup_url); + } + + git_str_clear(&buf); + + error = lookup_config(out, cfg, "http.proxy"); + +done: + git_config_free(cfg); + git_str_dispose(&buf); + git_net_url_dispose(&lookup_url); + return error; +} + +static int http_proxy_env(char **out, git_remote *remote, git_net_url *url) +{ + git_str proxy_env = GIT_STR_INIT, no_proxy_env = GIT_STR_INIT; + bool use_ssl = (strcmp(url->scheme, "https") == 0); + int error; + + GIT_UNUSED(remote); + + /* http_proxy / https_proxy environment variables */ + error = git__getenv(&proxy_env, use_ssl ? "https_proxy" : "http_proxy"); + + /* try uppercase environment variables */ + if (error == GIT_ENOTFOUND) + error = git__getenv(&proxy_env, use_ssl ? "HTTPS_PROXY" : "HTTP_PROXY"); + + if (error) + goto done; + + /* no_proxy/NO_PROXY environment variables */ + error = git__getenv(&no_proxy_env, "no_proxy"); + + if (error == GIT_ENOTFOUND) + error = git__getenv(&no_proxy_env, "NO_PROXY"); + + if (error && error != GIT_ENOTFOUND) + goto done; + + if (!git_net_url_matches_pattern_list(url, no_proxy_env.ptr)) + *out = git_str_detach(&proxy_env); + else + error = GIT_ENOTFOUND; + +done: + git_str_dispose(&proxy_env); + git_str_dispose(&no_proxy_env); + return error; +} + +int git_remote__http_proxy(char **out, git_remote *remote, git_net_url *url) +{ + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(remote); + + *out = NULL; + + /* + * Go through the possible sources for proxy configuration, + * Examine the various git config options first, then + * consult environment variables. + */ + if ((error = http_proxy_config(out, remote, url)) != GIT_ENOTFOUND || + (error = http_proxy_env(out, remote, url)) != GIT_ENOTFOUND) + return error; + + return 0; +} + +/* DWIM `refspecs` based on `refs` and append the output to `out` */ +static int dwim_refspecs(git_vector *out, git_vector *refspecs, git_vector *refs) +{ + size_t i; + git_refspec *spec; + + git_vector_foreach(refspecs, i, spec) { + if (git_refspec__dwim_one(out, spec, refs) < 0) + return -1; + } + + return 0; +} + +static void free_refspecs(git_vector *vec) +{ + size_t i; + git_refspec *spec; + + git_vector_foreach(vec, i, spec) { + git_refspec__dispose(spec); + git__free(spec); + } + + git_vector_clear(vec); +} + +static int remote_head_cmp(const void *_a, const void *_b) +{ + const git_remote_head *a = (git_remote_head *) _a; + const git_remote_head *b = (git_remote_head *) _b; + + return git__strcmp_cb(a->name, b->name); +} + +static int ls_to_vector(git_vector *out, git_remote *remote) +{ + git_remote_head **heads; + size_t heads_len, i; + + if (git_remote_ls((const git_remote_head ***)&heads, &heads_len, remote) < 0) + return -1; + + if (git_vector_init(out, heads_len, remote_head_cmp) < 0) + return -1; + + for (i = 0; i < heads_len; i++) { + if (git_vector_insert(out, heads[i]) < 0) + return -1; + } + + return 0; +} + +static int connect_or_reset_options( + git_remote *remote, + int direction, + git_remote_connect_options *opts) +{ + if (!git_remote_connected(remote)) { + return git_remote_connect_ext(remote, direction, opts); + } else { + return remote->transport->set_connect_opts(remote->transport, opts); + } +} + +/* Download from an already connected remote. */ +static int git_remote__download( + git_remote *remote, + const git_strarray *refspecs, + const git_fetch_options *opts) +{ + git_vector *to_active, specs = GIT_VECTOR_INIT, refs = GIT_VECTOR_INIT; + size_t i; + int error; + + if (ls_to_vector(&refs, remote) < 0) + return -1; + + if ((error = git_vector_init(&specs, 0, NULL)) < 0) + goto on_error; + + remote->passed_refspecs = 0; + if (!refspecs || !refspecs->count) { + to_active = &remote->refspecs; + } else { + for (i = 0; i < refspecs->count; i++) { + if ((error = add_refspec_to(&specs, refspecs->strings[i], true)) < 0) + goto on_error; + } + + to_active = &specs; + remote->passed_refspecs = 1; + } + + free_refspecs(&remote->passive_refspecs); + if ((error = dwim_refspecs(&remote->passive_refspecs, &remote->refspecs, &refs)) < 0) + goto on_error; + + free_refspecs(&remote->active_refspecs); + error = dwim_refspecs(&remote->active_refspecs, to_active, &refs); + + git_vector_free(&refs); + free_refspecs(&specs); + git_vector_free(&specs); + + if (error < 0) + goto on_error; + + if (remote->push) { + git_push_free(remote->push); + remote->push = NULL; + } + + if ((error = git_fetch_negotiate(remote, opts)) < 0) + goto on_error; + + error = git_fetch_download_pack(remote); + +on_error: + git_vector_free(&refs); + free_refspecs(&specs); + git_vector_free(&specs); + return error; +} + +int git_remote_download( + git_remote *remote, + const git_strarray *refspecs, + const git_fetch_options *opts) +{ + git_remote_connect_options connect_opts = GIT_REMOTE_CONNECT_OPTIONS_INIT; + int error; + + GIT_ASSERT_ARG(remote); + + if (!remote->repo) { + git_error_set(GIT_ERROR_INVALID, "cannot download detached remote"); + return -1; + } + + if (git_remote_connect_options__from_fetch_opts(&connect_opts, + remote, opts) < 0) + return -1; + + if ((error = connect_or_reset_options(remote, GIT_DIRECTION_FETCH, &connect_opts)) < 0) + return error; + + return git_remote__download(remote, refspecs, opts); +} + +int git_remote_fetch( + git_remote *remote, + const git_strarray *refspecs, + const git_fetch_options *opts, + const char *reflog_message) +{ + int error, update_fetchhead = 1; + git_remote_autotag_option_t tagopt = remote->download_tags; + bool prune = false; + git_str reflog_msg_buf = GIT_STR_INIT; + git_remote_connect_options connect_opts = GIT_REMOTE_CONNECT_OPTIONS_INIT; + unsigned int capabilities; + git_oid_t oid_type; + + GIT_ASSERT_ARG(remote); + + if (!remote->repo) { + git_error_set(GIT_ERROR_INVALID, "cannot download detached remote"); + return -1; + } + + if (git_remote_connect_options__from_fetch_opts(&connect_opts, + remote, opts) < 0) + return -1; + + if ((error = connect_or_reset_options(remote, GIT_DIRECTION_FETCH, &connect_opts)) < 0) + return error; + + if (opts) { + update_fetchhead = opts->update_fetchhead; + tagopt = opts->download_tags; + } + + if ((error = git_remote_capabilities(&capabilities, remote)) < 0 || + (error = git_remote_oid_type(&oid_type, remote)) < 0) + return error; + + /* Connect and download everything */ + error = git_remote__download(remote, refspecs, opts); + + /* We don't need to be connected anymore */ + git_remote_disconnect(remote); + + /* If the download failed, return the error */ + if (error != 0) + goto done; + + /* Default reflog message */ + if (reflog_message) + git_str_sets(&reflog_msg_buf, reflog_message); + else { + git_str_printf(&reflog_msg_buf, "fetch %s", + remote->name ? remote->name : remote->url); + } + + /* Create "remote/foo" branches for all remote branches */ + error = git_remote_update_tips(remote, &connect_opts.callbacks, update_fetchhead, tagopt, git_str_cstr(&reflog_msg_buf)); + git_str_dispose(&reflog_msg_buf); + if (error < 0) + goto done; + + if (opts && opts->prune == GIT_FETCH_PRUNE) + prune = true; + else if (opts && opts->prune == GIT_FETCH_PRUNE_UNSPECIFIED && remote->prune_refs) + prune = true; + else if (opts && opts->prune == GIT_FETCH_NO_PRUNE) + prune = false; + else + prune = remote->prune_refs; + + if (prune) + error = git_remote_prune(remote, &connect_opts.callbacks); + +done: + git_remote_connect_options_dispose(&connect_opts); + return error; +} + +static int remote_head_for_fetchspec_src(git_remote_head **out, git_vector *update_heads, const char *fetchspec_src) +{ + unsigned int i; + git_remote_head *remote_ref; + + GIT_ASSERT_ARG(update_heads); + GIT_ASSERT_ARG(fetchspec_src); + + *out = NULL; + + git_vector_foreach(update_heads, i, remote_ref) { + if (strcmp(remote_ref->name, fetchspec_src) == 0) { + *out = remote_ref; + break; + } + } + + return 0; +} + +static int ref_to_update(int *update, git_str *remote_name, git_remote *remote, git_refspec *spec, const char *ref_name) +{ + int error = 0; + git_repository *repo; + git_str upstream_remote = GIT_STR_INIT; + git_str upstream_name = GIT_STR_INIT; + + repo = git_remote_owner(remote); + + if ((!git_reference__is_branch(ref_name)) || + !git_remote_name(remote) || + (error = git_branch__upstream_remote(&upstream_remote, repo, ref_name) < 0) || + git__strcmp(git_remote_name(remote), git_str_cstr(&upstream_remote)) || + (error = git_branch__upstream_name(&upstream_name, repo, ref_name)) < 0 || + !git_refspec_dst_matches(spec, git_str_cstr(&upstream_name)) || + (error = git_refspec__rtransform(remote_name, spec, upstream_name.ptr)) < 0) { + /* Not an error if there is no upstream */ + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = 0; + } + + *update = 0; + } else { + *update = 1; + } + + git_str_dispose(&upstream_remote); + git_str_dispose(&upstream_name); + return error; +} + +static int remote_head_for_ref(git_remote_head **out, git_remote *remote, git_refspec *spec, git_vector *update_heads, git_reference *ref) +{ + git_reference *resolved_ref = NULL; + git_str remote_name = GIT_STR_INIT; + git_config *config = NULL; + const char *ref_name; + int error = 0, update; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(spec); + GIT_ASSERT_ARG(ref); + + *out = NULL; + + error = git_reference_resolve(&resolved_ref, ref); + + /* If we're in an unborn branch, let's pretend nothing happened */ + if (error == GIT_ENOTFOUND && git_reference_type(ref) == GIT_REFERENCE_SYMBOLIC) { + ref_name = git_reference_symbolic_target(ref); + error = 0; + } else { + ref_name = git_reference_name(resolved_ref); + } + + /* + * The ref name may be unresolvable - perhaps it's pointing to + * something invalid. In this case, there is no remote head for + * this ref. + */ + if (!ref_name) { + error = 0; + goto cleanup; + } + + if ((error = ref_to_update(&update, &remote_name, remote, spec, ref_name)) < 0) + goto cleanup; + + if (update) + error = remote_head_for_fetchspec_src(out, update_heads, git_str_cstr(&remote_name)); + +cleanup: + git_str_dispose(&remote_name); + git_reference_free(resolved_ref); + git_config_free(config); + return error; +} + +static int git_remote_write_fetchhead(git_remote *remote, git_refspec *spec, git_vector *update_heads) +{ + git_reference *head_ref = NULL; + git_fetchhead_ref *fetchhead_ref; + git_remote_head *remote_ref, *merge_remote_ref; + git_vector fetchhead_refs; + bool include_all_fetchheads; + unsigned int i = 0; + int error = 0; + + GIT_ASSERT_ARG(remote); + + /* no heads, nothing to do */ + if (update_heads->length == 0) + return 0; + + if (git_vector_init(&fetchhead_refs, update_heads->length, git_fetchhead_ref_cmp) < 0) + return -1; + + /* Iff refspec is * (but not subdir slash star), include tags */ + include_all_fetchheads = (strcmp(GIT_REFS_HEADS_DIR "*", git_refspec_src(spec)) == 0); + + /* Determine what to merge: if refspec was a wildcard, just use HEAD */ + if (git_refspec_is_wildcard(spec)) { + if ((error = git_reference_lookup(&head_ref, remote->repo, GIT_HEAD_FILE)) < 0 || + (error = remote_head_for_ref(&merge_remote_ref, remote, spec, update_heads, head_ref)) < 0) + goto cleanup; + } else { + /* If we're fetching a single refspec, that's the only thing that should be in FETCH_HEAD. */ + if ((error = remote_head_for_fetchspec_src(&merge_remote_ref, update_heads, git_refspec_src(spec))) < 0) + goto cleanup; + } + + /* Create the FETCH_HEAD file */ + git_vector_foreach(update_heads, i, remote_ref) { + int merge_this_fetchhead = (merge_remote_ref == remote_ref); + + if (!include_all_fetchheads && + !git_refspec_src_matches(spec, remote_ref->name) && + !merge_this_fetchhead) + continue; + + if (git_fetchhead_ref_create(&fetchhead_ref, + &remote_ref->oid, + merge_this_fetchhead, + remote_ref->name, + git_remote_url(remote)) < 0) + goto cleanup; + + if (git_vector_insert(&fetchhead_refs, fetchhead_ref) < 0) + goto cleanup; + } + + git_fetchhead_write(remote->repo, &fetchhead_refs); + +cleanup: + for (i = 0; i < fetchhead_refs.length; ++i) + git_fetchhead_ref_free(fetchhead_refs.contents[i]); + + git_vector_free(&fetchhead_refs); + git_reference_free(head_ref); + + return error; +} + +/** + * Generate a list of candidates for pruning by getting a list of + * references which match the rhs of an active refspec. + */ +static int prune_candidates(git_vector *candidates, git_remote *remote) +{ + git_strarray arr = { 0 }; + size_t i; + int error; + + if ((error = git_reference_list(&arr, remote->repo)) < 0) + return error; + + for (i = 0; i < arr.count; i++) { + const char *refname = arr.strings[i]; + char *refname_dup; + + if (!git_remote__matching_dst_refspec(remote, refname)) + continue; + + refname_dup = git__strdup(refname); + GIT_ERROR_CHECK_ALLOC(refname_dup); + + if ((error = git_vector_insert(candidates, refname_dup)) < 0) + goto out; + } + +out: + git_strarray_dispose(&arr); + return error; +} + +static int find_head(const void *_a, const void *_b) +{ + git_remote_head *a = (git_remote_head *) _a; + git_remote_head *b = (git_remote_head *) _b; + + return strcmp(a->name, b->name); +} + +int git_remote_prune(git_remote *remote, const git_remote_callbacks *callbacks) +{ + size_t i, j; + git_vector remote_refs = GIT_VECTOR_INIT; + git_vector candidates = GIT_VECTOR_INIT; + const git_refspec *spec; + const char *refname; + int error; + git_oid zero_id; + + GIT_ASSERT(remote && remote->repo); + git_oid_clear(&zero_id, remote->repo->oid_type); + + if (callbacks) + GIT_ERROR_CHECK_VERSION(callbacks, GIT_REMOTE_CALLBACKS_VERSION, "git_remote_callbacks"); + + if ((error = ls_to_vector(&remote_refs, remote)) < 0) + goto cleanup; + + git_vector_set_cmp(&remote_refs, find_head); + + if ((error = prune_candidates(&candidates, remote)) < 0) + goto cleanup; + + /* + * Remove those entries from the candidate list for which we + * can find a remote reference in at least one refspec. + */ + git_vector_foreach(&candidates, i, refname) { + git_vector_foreach(&remote->active_refspecs, j, spec) { + git_str buf = GIT_STR_INIT; + size_t pos; + char *src_name; + git_remote_head key = {0}; + + if (!git_refspec_dst_matches(spec, refname)) + continue; + + if ((error = git_refspec__rtransform(&buf, spec, refname)) < 0) + goto cleanup; + + key.name = (char *) git_str_cstr(&buf); + error = git_vector_bsearch(&pos, &remote_refs, &key); + git_str_dispose(&buf); + + if (error < 0 && error != GIT_ENOTFOUND) + goto cleanup; + + if (error == GIT_ENOTFOUND) + continue; + + /* If we did find a source, remove it from the candidates. */ + if ((error = git_vector_set((void **) &src_name, &candidates, i, NULL)) < 0) + goto cleanup; + + git__free(src_name); + break; + } + } + + /* + * For those candidates still left in the list, we need to + * remove them. We do not remove symrefs, as those are for + * stuff like origin/HEAD which will never match, but we do + * not want to remove them. + */ + git_vector_foreach(&candidates, i, refname) { + git_reference *ref; + git_oid id; + + if (refname == NULL) + continue; + + error = git_reference_lookup(&ref, remote->repo, refname); + /* as we want it gone, let's not consider this an error */ + if (error == GIT_ENOTFOUND) + continue; + + if (error < 0) + goto cleanup; + + if (git_reference_type(ref) == GIT_REFERENCE_SYMBOLIC) { + git_reference_free(ref); + continue; + } + + git_oid_cpy(&id, git_reference_target(ref)); + error = git_reference_delete(ref); + git_reference_free(ref); + if (error < 0) + goto cleanup; + + if (callbacks && callbacks->update_tips) + error = callbacks->update_tips(refname, &id, &zero_id, callbacks->payload); + + if (error < 0) + goto cleanup; + } + +cleanup: + git_vector_free(&remote_refs); + git_vector_free_deep(&candidates); + return error; +} + +static int update_ref( + const git_remote *remote, + const char *ref_name, + git_oid *id, + const char *msg, + const git_remote_callbacks *callbacks) +{ + git_reference *ref; + git_oid old_id; + int error; + + GIT_ASSERT(remote && remote->repo); + git_oid_clear(&old_id, remote->repo->oid_type); + + error = git_reference_name_to_id(&old_id, remote->repo, ref_name); + + if (error < 0 && error != GIT_ENOTFOUND) + return error; + else if (error == 0 && git_oid_equal(&old_id, id)) + return 0; + + /* If we did find a current reference, make sure we haven't lost a race */ + if (error) + error = git_reference_create(&ref, remote->repo, ref_name, id, true, msg); + else + error = git_reference_create_matching(&ref, remote->repo, ref_name, id, true, &old_id, msg); + + git_reference_free(ref); + + if (error < 0) + return error; + + if (callbacks && callbacks->update_tips && + (error = callbacks->update_tips(ref_name, &old_id, id, callbacks->payload)) < 0) + return error; + + return 0; +} + +static int update_one_tip( + git_vector *update_heads, + git_remote *remote, + git_refspec *spec, + git_remote_head *head, + git_refspec *tagspec, + git_remote_autotag_option_t tagopt, + const char *log_message, + const git_remote_callbacks *callbacks) +{ + git_odb *odb; + git_str refname = GIT_STR_INIT; + git_reference *ref = NULL; + bool autotag = false; + git_oid old; + int valid; + int error; + + GIT_ASSERT(remote && remote->repo); + + if ((error = git_repository_odb__weakptr(&odb, remote->repo)) < 0) + goto done; + + /* Ignore malformed ref names (which also saves us from tag^{} */ + if ((error = git_reference_name_is_valid(&valid, head->name)) < 0) + goto done; + + if (!valid) + goto done; + + /* If we have a tag, see if the auto-follow rules say to update it */ + if (git_refspec_src_matches(tagspec, head->name)) { + if (tagopt == GIT_REMOTE_DOWNLOAD_TAGS_AUTO) + autotag = true; + + if (tagopt != GIT_REMOTE_DOWNLOAD_TAGS_NONE) { + if (git_str_puts(&refname, head->name) < 0) + goto done; + } + } + + /* If we didn't want to auto-follow the tag, check if the refspec matches */ + if (!autotag && git_refspec_src_matches(spec, head->name)) { + if (spec->dst) { + if ((error = git_refspec__transform(&refname, spec, head->name)) < 0) + goto done; + } else { + /* + * no rhs means store it in FETCH_HEAD, even if we don't + * update anything else. + */ + error = git_vector_insert(update_heads, head); + goto done; + } + } + + /* If we still don't have a refname, we don't want it */ + if (git_str_len(&refname) == 0) + goto done; + + /* In autotag mode, only create tags for objects already in db */ + if (autotag && !git_odb_exists(odb, &head->oid)) + goto done; + + if (!autotag && (error = git_vector_insert(update_heads, head)) < 0) + goto done; + + error = git_reference_name_to_id(&old, remote->repo, refname.ptr); + + if (error < 0 && error != GIT_ENOTFOUND) + goto done; + + if (!(error || error == GIT_ENOTFOUND) && + !spec->force && + !git_graph_descendant_of(remote->repo, &head->oid, &old)) { + error = 0; + goto done; + } + + if (error == GIT_ENOTFOUND) { + git_oid_clear(&old, remote->repo->oid_type); + error = 0; + + if (autotag && (error = git_vector_insert(update_heads, head)) < 0) + goto done; + } + + if (!git_oid__cmp(&old, &head->oid)) + goto done; + + /* In autotag mode, don't overwrite any locally-existing tags */ + error = git_reference_create(&ref, remote->repo, refname.ptr, &head->oid, !autotag, + log_message); + + if (error < 0) { + if (error == GIT_EEXISTS) + error = 0; + + goto done; + } + + if (callbacks && callbacks->update_tips != NULL && + (error = callbacks->update_tips(refname.ptr, &old, &head->oid, callbacks->payload)) < 0) + git_error_set_after_callback_function(error, "git_remote_fetch"); + +done: + git_reference_free(ref); + git_str_dispose(&refname); + return error; +} + +static int update_tips_for_spec( + git_remote *remote, + const git_remote_callbacks *callbacks, + int update_fetchhead, + git_remote_autotag_option_t tagopt, + git_refspec *spec, + git_vector *refs, + const char *log_message) +{ + git_refspec tagspec; + git_remote_head *head, oid_head; + git_vector update_heads; + int error = 0; + size_t i; + + GIT_ASSERT_ARG(remote && remote->repo); + + if (git_refspec__parse(&tagspec, GIT_REFSPEC_TAGS, true) < 0) + return -1; + + /* Make a copy of the transport's refs */ + if (git_vector_init(&update_heads, 16, NULL) < 0) + return -1; + + /* Update tips based on the remote heads */ + git_vector_foreach(refs, i, head) { + if (update_one_tip(&update_heads, remote, spec, head, &tagspec, tagopt, log_message, callbacks) < 0) + goto on_error; + } + + /* Handle specified oid sources */ + if (git_oid__is_hexstr(spec->src, remote->repo->oid_type)) { + git_oid id; + + if ((error = git_oid__fromstr(&id, spec->src, remote->repo->oid_type)) < 0) + goto on_error; + + if (spec->dst && + (error = update_ref(remote, spec->dst, &id, log_message, callbacks)) < 0) + goto on_error; + + git_oid_cpy(&oid_head.oid, &id); + oid_head.name = spec->src; + + if ((error = git_vector_insert(&update_heads, &oid_head)) < 0) + goto on_error; + } + + if (update_fetchhead && + (error = git_remote_write_fetchhead(remote, spec, &update_heads)) < 0) + goto on_error; + + git_refspec__dispose(&tagspec); + git_vector_free(&update_heads); + return 0; + +on_error: + git_refspec__dispose(&tagspec); + git_vector_free(&update_heads); + return -1; + +} + +/** + * Iteration over the three vectors, with a pause whenever we find a match + * + * On each stop, we store the iteration stat in the inout i,j,k + * parameters, and return the currently matching passive refspec as + * well as the head which we matched. + */ +static int next_head(const git_remote *remote, git_vector *refs, + git_refspec **out_spec, git_remote_head **out_head, + size_t *out_i, size_t *out_j, size_t *out_k) +{ + const git_vector *active, *passive; + git_remote_head *head; + git_refspec *spec, *passive_spec; + size_t i, j, k; + int valid; + + active = &remote->active_refspecs; + passive = &remote->passive_refspecs; + + i = *out_i; + j = *out_j; + k = *out_k; + + for (; i < refs->length; i++) { + head = git_vector_get(refs, i); + + if (git_reference_name_is_valid(&valid, head->name) < 0) + return -1; + + if (!valid) + continue; + + for (; j < active->length; j++) { + spec = git_vector_get(active, j); + + if (!git_refspec_src_matches(spec, head->name)) + continue; + + for (; k < passive->length; k++) { + passive_spec = git_vector_get(passive, k); + + if (!git_refspec_src_matches(passive_spec, head->name)) + continue; + + *out_spec = passive_spec; + *out_head = head; + *out_i = i; + *out_j = j; + *out_k = k + 1; + return 0; + + } + k = 0; + } + j = 0; + } + + return GIT_ITEROVER; +} + +static int opportunistic_updates( + const git_remote *remote, + const git_remote_callbacks *callbacks, + git_vector *refs, + const char *msg) +{ + size_t i, j, k; + git_refspec *spec; + git_remote_head *head; + git_str refname = GIT_STR_INIT; + int error = 0; + + i = j = k = 0; + + /* Handle refspecs matching remote heads */ + while ((error = next_head(remote, refs, &spec, &head, &i, &j, &k)) == 0) { + /* + * If we got here, there is a refspec which was used + * for fetching which matches the source of one of the + * passive refspecs, so we should update that + * remote-tracking branch, but not add it to + * FETCH_HEAD + */ + + git_str_clear(&refname); + if ((error = git_refspec__transform(&refname, spec, head->name)) < 0 || + (error = update_ref(remote, refname.ptr, &head->oid, msg, callbacks)) < 0) + goto cleanup; + } + + if (error != GIT_ITEROVER) + goto cleanup; + + error = 0; + +cleanup: + git_str_dispose(&refname); + return error; +} + +static int truncate_fetch_head(const char *gitdir) +{ + git_str path = GIT_STR_INIT; + int error; + + if ((error = git_str_joinpath(&path, gitdir, GIT_FETCH_HEAD_FILE)) < 0) + return error; + + error = git_futils_truncate(path.ptr, GIT_REFS_FILE_MODE); + git_str_dispose(&path); + + return error; +} + +int git_remote_update_tips( + git_remote *remote, + const git_remote_callbacks *callbacks, + int update_fetchhead, + git_remote_autotag_option_t download_tags, + const char *reflog_message) +{ + git_refspec *spec, tagspec; + git_vector refs = GIT_VECTOR_INIT; + git_remote_autotag_option_t tagopt; + int error; + size_t i; + + /* push has its own logic hidden away in the push object */ + if (remote->push) { + return git_push_update_tips(remote->push, callbacks); + } + + if (git_refspec__parse(&tagspec, GIT_REFSPEC_TAGS, true) < 0) + return -1; + + + if ((error = ls_to_vector(&refs, remote)) < 0) + goto out; + + if (download_tags == GIT_REMOTE_DOWNLOAD_TAGS_UNSPECIFIED) + tagopt = remote->download_tags; + else + tagopt = download_tags; + + if ((error = truncate_fetch_head(git_repository_path(remote->repo))) < 0) + goto out; + + if (tagopt == GIT_REMOTE_DOWNLOAD_TAGS_ALL) { + if ((error = update_tips_for_spec(remote, callbacks, update_fetchhead, tagopt, &tagspec, &refs, reflog_message)) < 0) + goto out; + } + + git_vector_foreach(&remote->active_refspecs, i, spec) { + if (spec->push) + continue; + + if ((error = update_tips_for_spec(remote, callbacks, update_fetchhead, tagopt, spec, &refs, reflog_message)) < 0) + goto out; + } + + /* Only try to do opportunistic updates if the refspec lists differ. */ + if (remote->passed_refspecs) + error = opportunistic_updates(remote, callbacks, &refs, reflog_message); + +out: + git_vector_free(&refs); + git_refspec__dispose(&tagspec); + return error; +} + +int git_remote_connected(const git_remote *remote) +{ + GIT_ASSERT_ARG(remote); + + if (!remote->transport || !remote->transport->is_connected) + return 0; + + /* Ask the transport if it's connected. */ + return remote->transport->is_connected(remote->transport); +} + +int git_remote_stop(git_remote *remote) +{ + GIT_ASSERT_ARG(remote); + + if (remote->transport && remote->transport->cancel) + remote->transport->cancel(remote->transport); + + return 0; +} + +int git_remote_disconnect(git_remote *remote) +{ + GIT_ASSERT_ARG(remote); + + if (git_remote_connected(remote)) + remote->transport->close(remote->transport); + + return 0; +} + +static void free_heads(git_vector *heads) +{ + git_remote_head *head; + size_t i; + + git_vector_foreach(heads, i, head) { + git__free(head->name); + git__free(head); + } +} + +void git_remote_free(git_remote *remote) +{ + if (remote == NULL) + return; + + if (remote->transport != NULL) { + git_remote_disconnect(remote); + + remote->transport->free(remote->transport); + remote->transport = NULL; + } + + git_vector_free(&remote->refs); + + free_refspecs(&remote->refspecs); + git_vector_free(&remote->refspecs); + + free_refspecs(&remote->active_refspecs); + git_vector_free(&remote->active_refspecs); + + free_refspecs(&remote->passive_refspecs); + git_vector_free(&remote->passive_refspecs); + + free_heads(&remote->local_heads); + git_vector_free(&remote->local_heads); + + git_push_free(remote->push); + git__free(remote->url); + git__free(remote->pushurl); + git__free(remote->name); + git__free(remote); +} + +static int remote_list_cb(const git_config_entry *entry, void *payload) +{ + git_vector *list = payload; + const char *name = entry->name + strlen("remote."); + size_t namelen = strlen(name); + char *remote_name; + + /* we know name matches "remote..(push)?url" */ + + if (!strcmp(&name[namelen - 4], ".url")) + remote_name = git__strndup(name, namelen - 4); /* strip ".url" */ + else + remote_name = git__strndup(name, namelen - 8); /* strip ".pushurl" */ + GIT_ERROR_CHECK_ALLOC(remote_name); + + return git_vector_insert(list, remote_name); +} + +int git_remote_list(git_strarray *remotes_list, git_repository *repo) +{ + int error; + git_config *cfg; + git_vector list = GIT_VECTOR_INIT; + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; + + if ((error = git_vector_init(&list, 4, git__strcmp_cb)) < 0) + return error; + + error = git_config_foreach_match( + cfg, "^remote\\..*\\.(push)?url$", remote_list_cb, &list); + + if (error < 0) { + git_vector_free_deep(&list); + return error; + } + + git_vector_uniq(&list, git__free); + + remotes_list->strings = + (char **)git_vector_detach(&remotes_list->count, NULL, &list); + + return 0; +} + +const git_indexer_progress *git_remote_stats(git_remote *remote) +{ + GIT_ASSERT_ARG_WITH_RETVAL(remote, NULL); + return &remote->stats; +} + +git_remote_autotag_option_t git_remote_autotag(const git_remote *remote) +{ + return remote->download_tags; +} + +int git_remote_set_autotag(git_repository *repo, const char *remote, git_remote_autotag_option_t value) +{ + git_str var = GIT_STR_INIT; + git_config *config; + int error; + + GIT_ASSERT_ARG(repo && remote); + + if ((error = ensure_remote_name_is_valid(remote)) < 0) + return error; + + if ((error = git_repository_config__weakptr(&config, repo)) < 0) + return error; + + if ((error = git_str_printf(&var, CONFIG_TAGOPT_FMT, remote))) + return error; + + switch (value) { + case GIT_REMOTE_DOWNLOAD_TAGS_NONE: + error = git_config_set_string(config, var.ptr, "--no-tags"); + break; + case GIT_REMOTE_DOWNLOAD_TAGS_ALL: + error = git_config_set_string(config, var.ptr, "--tags"); + break; + case GIT_REMOTE_DOWNLOAD_TAGS_AUTO: + error = git_config_delete_entry(config, var.ptr); + if (error == GIT_ENOTFOUND) + error = 0; + break; + default: + git_error_set(GIT_ERROR_INVALID, "invalid value for the tagopt setting"); + error = -1; + } + + git_str_dispose(&var); + return error; +} + +int git_remote_prune_refs(const git_remote *remote) +{ + return remote->prune_refs; +} + +static int rename_remote_config_section( + git_repository *repo, + const char *old_name, + const char *new_name) +{ + git_str old_section_name = GIT_STR_INIT, + new_section_name = GIT_STR_INIT; + int error = -1; + + if (git_str_printf(&old_section_name, "remote.%s", old_name) < 0) + goto cleanup; + + if (new_name && + (git_str_printf(&new_section_name, "remote.%s", new_name) < 0)) + goto cleanup; + + error = git_config_rename_section( + repo, + git_str_cstr(&old_section_name), + new_name ? git_str_cstr(&new_section_name) : NULL); + +cleanup: + git_str_dispose(&old_section_name); + git_str_dispose(&new_section_name); + + return error; +} + +struct update_data { + git_config *config; + const char *old_remote_name; + const char *new_remote_name; +}; + +static int update_config_entries_cb( + const git_config_entry *entry, + void *payload) +{ + struct update_data *data = (struct update_data *)payload; + + if (strcmp(entry->value, data->old_remote_name)) + return 0; + + return git_config_set_string( + data->config, entry->name, data->new_remote_name); +} + +static int update_branch_remote_config_entry( + git_repository *repo, + const char *old_name, + const char *new_name) +{ + int error; + struct update_data data = { NULL }; + + if ((error = git_repository_config__weakptr(&data.config, repo)) < 0) + return error; + + data.old_remote_name = old_name; + data.new_remote_name = new_name; + + return git_config_foreach_match( + data.config, "branch\\..+\\.remote", update_config_entries_cb, &data); +} + +static int rename_one_remote_reference( + git_reference *reference_in, + const char *old_remote_name, + const char *new_remote_name) +{ + int error; + git_reference *ref = NULL, *dummy = NULL; + git_str namespace = GIT_STR_INIT, old_namespace = GIT_STR_INIT; + git_str new_name = GIT_STR_INIT; + git_str log_message = GIT_STR_INIT; + size_t pfx_len; + const char *target; + + if ((error = git_str_printf(&namespace, GIT_REFS_REMOTES_DIR "%s/", new_remote_name)) < 0) + return error; + + pfx_len = strlen(GIT_REFS_REMOTES_DIR) + strlen(old_remote_name) + 1; + git_str_puts(&new_name, namespace.ptr); + if ((error = git_str_puts(&new_name, git_reference_name(reference_in) + pfx_len)) < 0) + goto cleanup; + + if ((error = git_str_printf(&log_message, + "renamed remote %s to %s", + old_remote_name, new_remote_name)) < 0) + goto cleanup; + + if ((error = git_reference_rename(&ref, reference_in, git_str_cstr(&new_name), 1, + git_str_cstr(&log_message))) < 0) + goto cleanup; + + if (git_reference_type(ref) != GIT_REFERENCE_SYMBOLIC) + goto cleanup; + + /* Handle refs like origin/HEAD -> origin/master */ + target = git_reference_symbolic_target(ref); + if ((error = git_str_printf(&old_namespace, GIT_REFS_REMOTES_DIR "%s/", old_remote_name)) < 0) + goto cleanup; + + if (git__prefixcmp(target, old_namespace.ptr)) + goto cleanup; + + git_str_clear(&new_name); + git_str_puts(&new_name, namespace.ptr); + if ((error = git_str_puts(&new_name, target + pfx_len)) < 0) + goto cleanup; + + error = git_reference_symbolic_set_target(&dummy, ref, git_str_cstr(&new_name), + git_str_cstr(&log_message)); + + git_reference_free(dummy); + +cleanup: + git_reference_free(reference_in); + git_reference_free(ref); + git_str_dispose(&namespace); + git_str_dispose(&old_namespace); + git_str_dispose(&new_name); + git_str_dispose(&log_message); + return error; +} + +static int rename_remote_references( + git_repository *repo, + const char *old_name, + const char *new_name) +{ + int error; + git_str buf = GIT_STR_INIT; + git_reference *ref; + git_reference_iterator *iter; + + if ((error = git_str_printf(&buf, GIT_REFS_REMOTES_DIR "%s/*", old_name)) < 0) + return error; + + error = git_reference_iterator_glob_new(&iter, repo, git_str_cstr(&buf)); + git_str_dispose(&buf); + + if (error < 0) + return error; + + while ((error = git_reference_next(&ref, iter)) == 0) { + if ((error = rename_one_remote_reference(ref, old_name, new_name)) < 0) + break; + } + + git_reference_iterator_free(iter); + + return (error == GIT_ITEROVER) ? 0 : error; +} + +static int rename_fetch_refspecs(git_vector *problems, git_remote *remote, const char *new_name) +{ + git_config *config; + git_str base = GIT_STR_INIT, var = GIT_STR_INIT, val = GIT_STR_INIT; + const git_refspec *spec; + size_t i; + int error = 0; + + if ((error = git_repository_config__weakptr(&config, remote->repo)) < 0) + return error; + + if ((error = git_vector_init(problems, 1, NULL)) < 0) + return error; + + if ((error = default_fetchspec_for_name(&base, remote->name)) < 0) + return error; + + git_vector_foreach(&remote->refspecs, i, spec) { + if (spec->push) + continue; + + /* Does the dst part of the refspec follow the expected format? */ + if (strcmp(git_str_cstr(&base), spec->string)) { + char *dup; + + dup = git__strdup(spec->string); + GIT_ERROR_CHECK_ALLOC(dup); + + if ((error = git_vector_insert(problems, dup)) < 0) + break; + + continue; + } + + /* If we do want to move it to the new section */ + + git_str_clear(&val); + git_str_clear(&var); + + if (default_fetchspec_for_name(&val, new_name) < 0 || + git_str_printf(&var, "remote.%s.fetch", new_name) < 0) + { + error = -1; + break; + } + + if ((error = git_config_set_string( + config, git_str_cstr(&var), git_str_cstr(&val))) < 0) + break; + } + + git_str_dispose(&base); + git_str_dispose(&var); + git_str_dispose(&val); + + if (error < 0) { + char *str; + git_vector_foreach(problems, i, str) + git__free(str); + + git_vector_free(problems); + } + + return error; +} + +int git_remote_rename(git_strarray *out, git_repository *repo, const char *name, const char *new_name) +{ + int error; + git_vector problem_refspecs = GIT_VECTOR_INIT; + git_remote *remote = NULL; + + GIT_ASSERT_ARG(out && repo && name && new_name); + + if ((error = git_remote_lookup(&remote, repo, name)) < 0) + return error; + + if ((error = ensure_remote_name_is_valid(new_name)) < 0) + goto cleanup; + + if ((error = ensure_remote_doesnot_exist(repo, new_name)) < 0) + goto cleanup; + + if ((error = rename_remote_config_section(repo, name, new_name)) < 0) + goto cleanup; + + if ((error = update_branch_remote_config_entry(repo, name, new_name)) < 0) + goto cleanup; + + if ((error = rename_remote_references(repo, name, new_name)) < 0) + goto cleanup; + + if ((error = rename_fetch_refspecs(&problem_refspecs, remote, new_name)) < 0) + goto cleanup; + + out->count = problem_refspecs.length; + out->strings = (char **) problem_refspecs.contents; + +cleanup: + if (error < 0) + git_vector_free(&problem_refspecs); + + git_remote_free(remote); + return error; +} + +int git_remote_name_is_valid(int *valid, const char *remote_name) +{ + git_str buf = GIT_STR_INIT; + git_refspec refspec = {0}; + int error; + + GIT_ASSERT(valid); + + *valid = 0; + + if (!remote_name || *remote_name == '\0') + return 0; + + if ((error = git_str_printf(&buf, "refs/heads/test:refs/remotes/%s/test", remote_name)) < 0) + goto done; + + error = git_refspec__parse(&refspec, git_str_cstr(&buf), true); + + if (!error) + *valid = 1; + else if (error == GIT_EINVALIDSPEC) + error = 0; + +done: + git_str_dispose(&buf); + git_refspec__dispose(&refspec); + + return error; +} + +git_refspec *git_remote__matching_refspec(git_remote *remote, const char *refname) +{ + git_refspec *spec; + size_t i; + + git_vector_foreach(&remote->active_refspecs, i, spec) { + if (spec->push) + continue; + + if (git_refspec_src_matches(spec, refname)) + return spec; + } + + return NULL; +} + +git_refspec *git_remote__matching_dst_refspec(git_remote *remote, const char *refname) +{ + git_refspec *spec; + size_t i; + + git_vector_foreach(&remote->active_refspecs, i, spec) { + if (spec->push) + continue; + + if (git_refspec_dst_matches(spec, refname)) + return spec; + } + + return NULL; +} + +int git_remote_add_fetch(git_repository *repo, const char *remote, const char *refspec) +{ + return write_add_refspec(repo, remote, refspec, true); +} + +int git_remote_add_push(git_repository *repo, const char *remote, const char *refspec) +{ + return write_add_refspec(repo, remote, refspec, false); +} + +static int copy_refspecs(git_strarray *array, const git_remote *remote, unsigned int push) +{ + size_t i; + git_vector refspecs; + git_refspec *spec; + char *dup; + + if (git_vector_init(&refspecs, remote->refspecs.length, NULL) < 0) + return -1; + + git_vector_foreach(&remote->refspecs, i, spec) { + if (spec->push != push) + continue; + + if ((dup = git__strdup(spec->string)) == NULL) + goto on_error; + + if (git_vector_insert(&refspecs, dup) < 0) { + git__free(dup); + goto on_error; + } + } + + array->strings = (char **)refspecs.contents; + array->count = refspecs.length; + + return 0; + +on_error: + git_vector_free_deep(&refspecs); + + return -1; +} + +int git_remote_get_fetch_refspecs(git_strarray *array, const git_remote *remote) +{ + return copy_refspecs(array, remote, false); +} + +int git_remote_get_push_refspecs(git_strarray *array, const git_remote *remote) +{ + return copy_refspecs(array, remote, true); +} + +size_t git_remote_refspec_count(const git_remote *remote) +{ + return remote->refspecs.length; +} + +const git_refspec *git_remote_get_refspec(const git_remote *remote, size_t n) +{ + return git_vector_get(&remote->refspecs, n); +} + +int git_remote_init_callbacks(git_remote_callbacks *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_remote_callbacks, GIT_REMOTE_CALLBACKS_INIT); + return 0; +} + +/* asserts a branch..remote format */ +static const char *name_offset(size_t *len_out, const char *name) +{ + size_t prefix_len; + const char *dot; + + prefix_len = strlen("remote."); + dot = strchr(name + prefix_len, '.'); + + GIT_ASSERT_ARG_WITH_RETVAL(dot, NULL); + + *len_out = dot - name - prefix_len; + return name + prefix_len; +} + +static int remove_branch_config_related_entries( + git_repository *repo, + const char *remote_name) +{ + int error; + git_config *config; + git_config_entry *entry; + git_config_iterator *iter; + git_str buf = GIT_STR_INIT; + + if ((error = git_repository_config__weakptr(&config, repo)) < 0) + return error; + + if ((error = git_config_iterator_glob_new(&iter, config, "branch\\..+\\.remote")) < 0) + return error; + + /* find any branches with us as upstream and remove that config */ + while ((error = git_config_next(&entry, iter)) == 0) { + const char *branch; + size_t branch_len; + + if (strcmp(remote_name, entry->value)) + continue; + + if ((branch = name_offset(&branch_len, entry->name)) == NULL) { + error = -1; + break; + } + + git_str_clear(&buf); + if ((error = git_str_printf(&buf, "branch.%.*s.merge", (int)branch_len, branch)) < 0) + break; + + if ((error = git_config_delete_entry(config, git_str_cstr(&buf))) < 0) { + if (error != GIT_ENOTFOUND) + break; + git_error_clear(); + } + + git_str_clear(&buf); + if ((error = git_str_printf(&buf, "branch.%.*s.remote", (int)branch_len, branch)) < 0) + break; + + if ((error = git_config_delete_entry(config, git_str_cstr(&buf))) < 0) { + if (error != GIT_ENOTFOUND) + break; + git_error_clear(); + } + } + + if (error == GIT_ITEROVER) + error = 0; + + git_str_dispose(&buf); + git_config_iterator_free(iter); + return error; +} + +static int remove_refs(git_repository *repo, const git_refspec *spec) +{ + git_reference_iterator *iter = NULL; + git_vector refs; + const char *name; + char *dup; + int error; + size_t i; + + if ((error = git_vector_init(&refs, 8, NULL)) < 0) + return error; + + if ((error = git_reference_iterator_new(&iter, repo)) < 0) + goto cleanup; + + while ((error = git_reference_next_name(&name, iter)) == 0) { + if (!git_refspec_dst_matches(spec, name)) + continue; + + dup = git__strdup(name); + if (!dup) { + error = -1; + goto cleanup; + } + + if ((error = git_vector_insert(&refs, dup)) < 0) + goto cleanup; + } + if (error == GIT_ITEROVER) + error = 0; + if (error < 0) + goto cleanup; + + git_vector_foreach(&refs, i, name) { + if ((error = git_reference_remove(repo, name)) < 0) + break; + } + +cleanup: + git_reference_iterator_free(iter); + git_vector_foreach(&refs, i, dup) { + git__free(dup); + } + git_vector_free(&refs); + return error; +} + +static int remove_remote_tracking(git_repository *repo, const char *remote_name) +{ + git_remote *remote; + int error; + size_t i, count; + + /* we want to use what's on the config, regardless of changes to the instance in memory */ + if ((error = git_remote_lookup(&remote, repo, remote_name)) < 0) + return error; + + count = git_remote_refspec_count(remote); + for (i = 0; i < count; i++) { + const git_refspec *refspec = git_remote_get_refspec(remote, i); + + /* shouldn't ever actually happen */ + if (refspec == NULL) + continue; + + if ((error = remove_refs(repo, refspec)) < 0) + break; + } + + git_remote_free(remote); + return error; +} + +int git_remote_delete(git_repository *repo, const char *name) +{ + int error; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + if ((error = remove_branch_config_related_entries(repo, name)) < 0 || + (error = remove_remote_tracking(repo, name)) < 0 || + (error = rename_remote_config_section(repo, name, NULL)) < 0) + return error; + + return 0; +} + +int git_remote_default_branch(git_buf *out, git_remote *remote) +{ + GIT_BUF_WRAP_PRIVATE(out, git_remote__default_branch, remote); +} + +int git_remote__default_branch(git_str *out, git_remote *remote) +{ + const git_remote_head **heads; + const git_remote_head *guess = NULL; + const git_oid *head_id; + size_t heads_len, i; + git_str local_default = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(out); + + if ((error = git_remote_ls(&heads, &heads_len, remote)) < 0) + goto done; + + if (heads_len == 0 || strcmp(heads[0]->name, GIT_HEAD_FILE)) { + error = GIT_ENOTFOUND; + goto done; + } + + /* the first one must be HEAD so if that has the symref info, we're done */ + if (heads[0]->symref_target) { + error = git_str_puts(out, heads[0]->symref_target); + goto done; + } + + /* + * If there's no symref information, we have to look over them + * and guess. We return the first match unless the default + * branch is a candidate. Then we return the default branch. + */ + + if ((error = git_repository_initialbranch(&local_default, remote->repo)) < 0) + goto done; + + head_id = &heads[0]->oid; + + for (i = 1; i < heads_len; i++) { + if (git_oid_cmp(head_id, &heads[i]->oid)) + continue; + + if (git__prefixcmp(heads[i]->name, GIT_REFS_HEADS_DIR)) + continue; + + if (!guess) { + guess = heads[i]; + continue; + } + + if (!git__strcmp(local_default.ptr, heads[i]->name)) { + guess = heads[i]; + break; + } + } + + if (!guess) { + error = GIT_ENOTFOUND; + goto done; + } + + error = git_str_puts(out, guess->name); + +done: + git_str_dispose(&local_default); + return error; +} + +int git_remote_upload( + git_remote *remote, + const git_strarray *refspecs, + const git_push_options *opts) +{ + git_remote_connect_options connect_opts = GIT_REMOTE_CONNECT_OPTIONS_INIT; + git_push *push; + git_refspec *spec; + size_t i; + int error; + + GIT_ASSERT_ARG(remote); + + if (!remote->repo) { + git_error_set(GIT_ERROR_INVALID, "cannot download detached remote"); + return -1; + } + + if ((error = git_remote_connect_options__from_push_opts( + &connect_opts, remote, opts)) < 0) + goto cleanup; + + if ((error = connect_or_reset_options(remote, GIT_DIRECTION_PUSH, &connect_opts)) < 0) + goto cleanup; + + free_refspecs(&remote->active_refspecs); + if ((error = dwim_refspecs(&remote->active_refspecs, &remote->refspecs, &remote->refs)) < 0) + goto cleanup; + + if (remote->push) { + git_push_free(remote->push); + remote->push = NULL; + } + + if ((error = git_push_new(&remote->push, remote, opts)) < 0) + goto cleanup; + + push = remote->push; + + if (refspecs && refspecs->count > 0) { + for (i = 0; i < refspecs->count; i++) { + if ((error = git_push_add_refspec(push, refspecs->strings[i])) < 0) + goto cleanup; + } + } else { + git_vector_foreach(&remote->refspecs, i, spec) { + if (!spec->push) + continue; + if ((error = git_push_add_refspec(push, spec->string)) < 0) + goto cleanup; + } + } + + if ((error = git_push_finish(push)) < 0) + goto cleanup; + + if (connect_opts.callbacks.push_update_reference && + (error = git_push_status_foreach(push, connect_opts.callbacks.push_update_reference, connect_opts.callbacks.payload)) < 0) + goto cleanup; + +cleanup: + git_remote_connect_options_dispose(&connect_opts); + return error; +} + +int git_remote_push( + git_remote *remote, + const git_strarray *refspecs, + const git_push_options *opts) +{ + git_remote_connect_options connect_opts = GIT_REMOTE_CONNECT_OPTIONS_INIT; + int error; + + GIT_ASSERT_ARG(remote); + + if (!remote->repo) { + git_error_set(GIT_ERROR_INVALID, "cannot download detached remote"); + return -1; + } + + if (git_remote_connect_options__from_push_opts(&connect_opts, + remote, opts) < 0) + return -1; + + if ((error = git_remote_upload(remote, refspecs, opts)) < 0) + goto done; + + error = git_remote_update_tips(remote, &connect_opts.callbacks, 0, 0, NULL); + +done: + git_remote_disconnect(remote); + git_remote_connect_options_dispose(&connect_opts); + return error; +} + +#define PREFIX "url" +#define SUFFIX_FETCH "insteadof" +#define SUFFIX_PUSH "pushinsteadof" + +static int apply_insteadof(char **out, git_config *config, const char *url, int direction, bool use_default_if_empty) +{ + size_t match_length, prefix_length, suffix_length; + char *replacement = NULL; + const char *regexp; + + git_str result = GIT_STR_INIT; + git_config_entry *entry; + git_config_iterator *iter; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(config); + GIT_ASSERT_ARG(url); + GIT_ASSERT_ARG(direction == GIT_DIRECTION_FETCH || direction == GIT_DIRECTION_PUSH); + + /* Add 1 to prefix/suffix length due to the additional escaped dot */ + prefix_length = strlen(PREFIX) + 1; + if (direction == GIT_DIRECTION_FETCH) { + regexp = PREFIX "\\..*\\." SUFFIX_FETCH; + suffix_length = strlen(SUFFIX_FETCH) + 1; + } else { + regexp = PREFIX "\\..*\\." SUFFIX_PUSH; + suffix_length = strlen(SUFFIX_PUSH) + 1; + } + + if (git_config_iterator_glob_new(&iter, config, regexp) < 0) + return -1; + + match_length = 0; + while (git_config_next(&entry, iter) == 0) { + size_t n, replacement_length; + + /* Check if entry value is a prefix of URL */ + if (git__prefixcmp(url, entry->value)) + continue; + + /* Check if entry value is longer than previous + * prefixes */ + if ((n = strlen(entry->value)) <= match_length) + continue; + + git__free(replacement); + match_length = n; + + /* Cut off prefix and suffix of the value */ + replacement_length = + strlen(entry->name) - (prefix_length + suffix_length); + replacement = git__strndup(entry->name + prefix_length, + replacement_length); + } + + git_config_iterator_free(iter); + + if (match_length == 0 && use_default_if_empty) { + *out = git__strdup(url); + return *out ? 0 : -1; + } else if (match_length == 0) { + *out = NULL; + return 0; + } + + git_str_printf(&result, "%s%s", replacement, url + match_length); + + git__free(replacement); + + *out = git_str_detach(&result); + return 0; +} + +/* Deprecated functions */ + +#ifndef GIT_DEPRECATE_HARD + +int git_remote_is_valid_name(const char *remote_name) +{ + int valid = 0; + + git_remote_name_is_valid(&valid, remote_name); + return valid; +} + +#endif diff --git a/src/libgit2/remote.h b/src/libgit2/remote.h new file mode 100644 index 0000000..9e089be --- /dev/null +++ b/src/libgit2/remote.h @@ -0,0 +1,101 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_remote_h__ +#define INCLUDE_remote_h__ + +#include "common.h" + +#include "git2/remote.h" +#include "git2/transport.h" +#include "git2/sys/remote.h" +#include "git2/sys/transport.h" + +#include "refspec.h" +#include "vector.h" +#include "net.h" +#include "proxy.h" + +#define GIT_REMOTE_ORIGIN "origin" + +struct git_remote { + char *name; + char *url; + char *pushurl; + git_vector refs; + git_vector refspecs; + git_vector active_refspecs; + git_vector passive_refspecs; + git_vector local_heads; + git_transport *transport; + git_repository *repo; + git_push *push; + git_indexer_progress stats; + unsigned int need_pack; + git_remote_autotag_option_t download_tags; + int prune_refs; + int passed_refspecs; + git_fetch_negotiation nego; +}; + +int git_remote__urlfordirection(git_str *url_out, struct git_remote *remote, int direction, const git_remote_callbacks *callbacks); +int git_remote__http_proxy(char **out, git_remote *remote, git_net_url *url); + +git_refspec *git_remote__matching_refspec(git_remote *remote, const char *refname); +git_refspec *git_remote__matching_dst_refspec(git_remote *remote, const char *refname); + +int git_remote__default_branch(git_str *out, git_remote *remote); + +int git_remote_connect_options_dup( + git_remote_connect_options *dst, + const git_remote_connect_options *src); +int git_remote_connect_options_normalize( + git_remote_connect_options *dst, + git_repository *repo, + const git_remote_connect_options *src); + +int git_remote_capabilities(unsigned int *out, git_remote *remote); +int git_remote_oid_type(git_oid_t *out, git_remote *remote); + + +#define git_remote_connect_options__copy_opts(out, in) \ + if (in) { \ + (out)->callbacks = (in)->callbacks; \ + (out)->proxy_opts = (in)->proxy_opts; \ + (out)->custom_headers = (in)->custom_headers; \ + (out)->follow_redirects = (in)->follow_redirects; \ + } + +GIT_INLINE(int) git_remote_connect_options__from_fetch_opts( + git_remote_connect_options *out, + git_remote *remote, + const git_fetch_options *fetch_opts) +{ + git_remote_connect_options tmp = GIT_REMOTE_CONNECT_OPTIONS_INIT; + git_remote_connect_options__copy_opts(&tmp, fetch_opts); + return git_remote_connect_options_normalize(out, remote->repo, &tmp); +} + +GIT_INLINE(int) git_remote_connect_options__from_push_opts( + git_remote_connect_options *out, + git_remote *remote, + const git_push_options *push_opts) +{ + git_remote_connect_options tmp = GIT_REMOTE_CONNECT_OPTIONS_INIT; + git_remote_connect_options__copy_opts(&tmp, push_opts); + return git_remote_connect_options_normalize(out, remote->repo, &tmp); +} + +#undef git_remote_connect_options__copy_opts + +GIT_INLINE(void) git_remote_connect_options__dispose( + git_remote_connect_options *opts) +{ + git_proxy_options_dispose(&opts->proxy_opts); + git_strarray_dispose(&opts->custom_headers); +} + +#endif diff --git a/src/libgit2/repo_template.h b/src/libgit2/repo_template.h new file mode 100644 index 0000000..099279a --- /dev/null +++ b/src/libgit2/repo_template.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_repo_template_h__ +#define INCLUDE_repo_template_h__ + +#define GIT_OBJECTS_INFO_DIR GIT_OBJECTS_DIR "info/" +#define GIT_OBJECTS_PACK_DIR GIT_OBJECTS_DIR "pack/" + +#define GIT_HOOKS_DIR "hooks/" +#define GIT_HOOKS_DIR_MODE 0777 + +#define GIT_HOOKS_README_FILE GIT_HOOKS_DIR "README.sample" +#define GIT_HOOKS_README_MODE 0777 +#define GIT_HOOKS_README_CONTENT \ +"#!/bin/sh\n"\ +"#\n"\ +"# Place appropriately named executable hook scripts into this directory\n"\ +"# to intercept various actions that git takes. See `git help hooks` for\n"\ +"# more information.\n" + +#define GIT_INFO_DIR "info/" +#define GIT_INFO_DIR_MODE 0777 + +#define GIT_INFO_EXCLUDE_FILE GIT_INFO_DIR "exclude" +#define GIT_INFO_EXCLUDE_MODE 0666 +#define GIT_INFO_EXCLUDE_CONTENT \ +"# File patterns to ignore; see `git help ignore` for more information.\n"\ +"# Lines that start with '#' are comments.\n" + +#define GIT_DESC_FILE "description" +#define GIT_DESC_MODE 0666 +#define GIT_DESC_CONTENT \ +"Unnamed repository; edit this file 'description' to name the repository.\n" + +typedef struct { + const char *path; + mode_t mode; + const char *content; +} repo_template_item; + +static repo_template_item repo_template[] = { + { GIT_OBJECTS_INFO_DIR, GIT_OBJECT_DIR_MODE, NULL }, /* '/objects/info/' */ + { GIT_OBJECTS_PACK_DIR, GIT_OBJECT_DIR_MODE, NULL }, /* '/objects/pack/' */ + { GIT_REFS_HEADS_DIR, GIT_REFS_DIR_MODE, NULL }, /* '/refs/heads/' */ + { GIT_REFS_TAGS_DIR, GIT_REFS_DIR_MODE, NULL }, /* '/refs/tags/' */ + { GIT_HOOKS_DIR, GIT_HOOKS_DIR_MODE, NULL }, /* '/hooks/' */ + { GIT_INFO_DIR, GIT_INFO_DIR_MODE, NULL }, /* '/info/' */ + { GIT_DESC_FILE, GIT_DESC_MODE, GIT_DESC_CONTENT }, + { GIT_HOOKS_README_FILE, GIT_HOOKS_README_MODE, GIT_HOOKS_README_CONTENT }, + { GIT_INFO_EXCLUDE_FILE, GIT_INFO_EXCLUDE_MODE, GIT_INFO_EXCLUDE_CONTENT }, + { NULL, 0, NULL } +}; + +#endif diff --git a/src/libgit2/repository.c b/src/libgit2/repository.c new file mode 100644 index 0000000..05ece6e --- /dev/null +++ b/src/libgit2/repository.c @@ -0,0 +1,3841 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "repository.h" + +#include + +#include "git2/object.h" +#include "git2/sys/repository.h" + +#include "buf.h" +#include "common.h" +#include "commit.h" +#include "grafts.h" +#include "tag.h" +#include "blob.h" +#include "futils.h" +#include "sysdir.h" +#include "filebuf.h" +#include "index.h" +#include "config.h" +#include "refs.h" +#include "filter.h" +#include "odb.h" +#include "refdb.h" +#include "remote.h" +#include "merge.h" +#include "diff_driver.h" +#include "annotated_commit.h" +#include "submodule.h" +#include "worktree.h" +#include "path.h" +#include "strmap.h" + +#ifdef GIT_WIN32 +# include "win32/w32_util.h" +#endif + +bool git_repository__validate_ownership = true; +bool git_repository__fsync_gitdir = false; + +static const struct { + git_repository_item_t parent; + git_repository_item_t fallback; + const char *name; + bool directory; +} items[] = { + { GIT_REPOSITORY_ITEM_GITDIR, GIT_REPOSITORY_ITEM__LAST, NULL, true }, + { GIT_REPOSITORY_ITEM_WORKDIR, GIT_REPOSITORY_ITEM__LAST, NULL, true }, + { GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM__LAST, NULL, true }, + { GIT_REPOSITORY_ITEM_GITDIR, GIT_REPOSITORY_ITEM__LAST, "index", false }, + { GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "objects", true }, + { GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "refs", true }, + { GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "packed-refs", false }, + { GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "remotes", true }, + { GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "config", false }, + { GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "info", true }, + { GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "hooks", true }, + { GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "logs", true }, + { GIT_REPOSITORY_ITEM_GITDIR, GIT_REPOSITORY_ITEM__LAST, "modules", true }, + { GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "worktrees", true } +}; + +static int check_repositoryformatversion(int *version, git_config *config); +static int check_extensions(git_config *config, int version); +static int load_global_config(git_config **config, bool use_env); +static int load_objectformat(git_repository *repo, git_config *config); + +#define GIT_COMMONDIR_FILE "commondir" +#define GIT_GITDIR_FILE "gitdir" + +#define GIT_FILE_CONTENT_PREFIX "gitdir:" + +#define GIT_BRANCH_DEFAULT "master" + +#define GIT_REPO_VERSION_DEFAULT 0 +#define GIT_REPO_VERSION_MAX 1 + +git_str git_repository__reserved_names_win32[] = { + { DOT_GIT, 0, CONST_STRLEN(DOT_GIT) }, + { GIT_DIR_SHORTNAME, 0, CONST_STRLEN(GIT_DIR_SHORTNAME) } +}; +size_t git_repository__reserved_names_win32_len = 2; + +git_str git_repository__reserved_names_posix[] = { + { DOT_GIT, 0, CONST_STRLEN(DOT_GIT) }, +}; +size_t git_repository__reserved_names_posix_len = 1; + +static void set_odb(git_repository *repo, git_odb *odb) +{ + if (odb) { + GIT_REFCOUNT_OWN(odb, repo); + GIT_REFCOUNT_INC(odb); + } + + if ((odb = git_atomic_swap(repo->_odb, odb)) != NULL) { + GIT_REFCOUNT_OWN(odb, NULL); + git_odb_free(odb); + } +} + +static void set_refdb(git_repository *repo, git_refdb *refdb) +{ + if (refdb) { + GIT_REFCOUNT_OWN(refdb, repo); + GIT_REFCOUNT_INC(refdb); + } + + if ((refdb = git_atomic_swap(repo->_refdb, refdb)) != NULL) { + GIT_REFCOUNT_OWN(refdb, NULL); + git_refdb_free(refdb); + } +} + +static void set_config(git_repository *repo, git_config *config) +{ + if (config) { + GIT_REFCOUNT_OWN(config, repo); + GIT_REFCOUNT_INC(config); + } + + if ((config = git_atomic_swap(repo->_config, config)) != NULL) { + GIT_REFCOUNT_OWN(config, NULL); + git_config_free(config); + } + + git_repository__configmap_lookup_cache_clear(repo); +} + +static void set_index(git_repository *repo, git_index *index) +{ + if (index) { + GIT_REFCOUNT_OWN(index, repo); + GIT_REFCOUNT_INC(index); + } + + if ((index = git_atomic_swap(repo->_index, index)) != NULL) { + GIT_REFCOUNT_OWN(index, NULL); + git_index_free(index); + } +} + +int git_repository__cleanup(git_repository *repo) +{ + GIT_ASSERT_ARG(repo); + + git_repository_submodule_cache_clear(repo); + git_cache_clear(&repo->objects); + git_attr_cache_flush(repo); + git_grafts_free(repo->grafts); + repo->grafts = NULL; + git_grafts_free(repo->shallow_grafts); + repo->shallow_grafts = NULL; + + set_config(repo, NULL); + set_index(repo, NULL); + set_odb(repo, NULL); + set_refdb(repo, NULL); + + return 0; +} + +void git_repository_free(git_repository *repo) +{ + size_t i; + + if (repo == NULL) + return; + + git_repository__cleanup(repo); + + git_cache_dispose(&repo->objects); + + git_diff_driver_registry_free(repo->diff_drivers); + repo->diff_drivers = NULL; + + for (i = 0; i < repo->reserved_names.size; i++) + git_str_dispose(git_array_get(repo->reserved_names, i)); + git_array_clear(repo->reserved_names); + + git__free(repo->gitlink); + git__free(repo->gitdir); + git__free(repo->commondir); + git__free(repo->workdir); + git__free(repo->namespace); + git__free(repo->ident_name); + git__free(repo->ident_email); + + git__memzero(repo, sizeof(*repo)); + git__free(repo); +} + +/* Check if we have a separate commondir (e.g. we have a worktree) */ +static int lookup_commondir( + bool *separate, + git_str *commondir, + git_str *repository_path, + uint32_t flags) +{ + git_str common_link = GIT_STR_INIT; + int error; + + /* Environment variable overrides configuration */ + if ((flags & GIT_REPOSITORY_OPEN_FROM_ENV)) { + error = git__getenv(commondir, "GIT_COMMON_DIR"); + + if (!error || error != GIT_ENOTFOUND) + goto done; + } + + /* + * If there's no commondir file, the repository path is the + * common path, but it needs a trailing slash. + */ + if (!git_fs_path_contains_file(repository_path, GIT_COMMONDIR_FILE)) { + if ((error = git_str_set(commondir, repository_path->ptr, repository_path->size)) == 0) + error = git_fs_path_to_dir(commondir); + + *separate = false; + goto done; + } + + *separate = true; + + if ((error = git_str_joinpath(&common_link, repository_path->ptr, GIT_COMMONDIR_FILE)) < 0 || + (error = git_futils_readbuffer(&common_link, common_link.ptr)) < 0) + goto done; + + git_str_rtrim(&common_link); + if (git_fs_path_is_relative(common_link.ptr)) { + if ((error = git_str_joinpath(commondir, repository_path->ptr, common_link.ptr)) < 0) + goto done; + } else { + git_str_swap(commondir, &common_link); + } + + /* Make sure the commondir path always has a trailing slash */ + error = git_fs_path_prettify_dir(commondir, commondir->ptr, NULL); + +done: + git_str_dispose(&common_link); + return error; +} + +GIT_INLINE(int) validate_repo_path(git_str *path) +{ + /* + * The longest static path in a repository (or commondir) is the + * packed refs file. (Loose refs may be longer since they + * include the reference name, but will be validated when the + * path is constructed.) + */ + static size_t suffix_len = + CONST_STRLEN("objects/pack/pack-.pack.lock") + + GIT_OID_MAX_HEXSIZE; + + return git_fs_path_validate_str_length_with_suffix( + path, suffix_len); +} + +/* + * Git repository open methods + * + * Open a repository object from its path + */ +static int is_valid_repository_path( + bool *out, + git_str *repository_path, + git_str *common_path, + uint32_t flags) +{ + bool separate_commondir = false; + int error; + + *out = false; + + if ((error = lookup_commondir(&separate_commondir, + common_path, repository_path, flags)) < 0) + return error; + + /* Ensure HEAD file exists */ + if (git_fs_path_contains_file(repository_path, GIT_HEAD_FILE) == false) + return 0; + + /* Check files in common dir */ + if (git_fs_path_contains_dir(common_path, GIT_OBJECTS_DIR) == false) + return 0; + if (git_fs_path_contains_dir(common_path, GIT_REFS_DIR) == false) + return 0; + + /* Ensure the repo (and commondir) are valid paths */ + if ((error = validate_repo_path(common_path)) < 0 || + (separate_commondir && + (error = validate_repo_path(repository_path)) < 0)) + return error; + + *out = true; + return 0; +} + +static git_repository *repository_alloc(void) +{ + git_repository *repo = git__calloc(1, sizeof(git_repository)); + + if (repo == NULL || + git_cache_init(&repo->objects) < 0) + goto on_error; + + git_array_init_to_size(repo->reserved_names, 4); + if (!repo->reserved_names.ptr) + goto on_error; + + /* set all the entries in the configmap cache to `unset` */ + git_repository__configmap_lookup_cache_clear(repo); + + return repo; + +on_error: + if (repo) + git_cache_dispose(&repo->objects); + + git__free(repo); + return NULL; +} + +int git_repository_new(git_repository **out) +{ + git_repository *repo; + + *out = repo = repository_alloc(); + GIT_ERROR_CHECK_ALLOC(repo); + + repo->is_bare = 1; + repo->is_worktree = 0; + + return 0; +} + +static int load_config_data(git_repository *repo, const git_config *config) +{ + int is_bare; + + int err = git_config_get_bool(&is_bare, config, "core.bare"); + if (err < 0 && err != GIT_ENOTFOUND) + return err; + + /* Try to figure out if it's bare, default to non-bare if it's not set */ + if (err != GIT_ENOTFOUND) + repo->is_bare = is_bare && !repo->is_worktree; + else + repo->is_bare = 0; + + return 0; +} + +static int load_workdir( + git_repository *repo, + git_config *config, + git_str *parent_path) +{ + git_config_entry *ce = NULL; + git_str worktree = GIT_STR_INIT; + git_str path = GIT_STR_INIT; + git_str workdir_env = GIT_STR_INIT; + const char *value = NULL; + int error; + + if (repo->is_bare) + return 0; + + /* Environment variables are preferred */ + if (repo->use_env) { + error = git__getenv(&workdir_env, "GIT_WORK_TREE"); + + if (error == 0) + value = workdir_env.ptr; + else if (error == GIT_ENOTFOUND) + error = 0; + else + goto cleanup; + } + + /* Examine configuration values if necessary */ + if (!value) { + if ((error = git_config__lookup_entry(&ce, config, + "core.worktree", false)) < 0) + return error; + + if (ce && ce->value) + value = ce->value; + } + + if (repo->is_worktree) { + char *gitlink = git_worktree__read_link(repo->gitdir, GIT_GITDIR_FILE); + if (!gitlink) { + error = -1; + goto cleanup; + } + + git_str_attach(&worktree, gitlink, 0); + + if ((git_fs_path_dirname_r(&worktree, worktree.ptr)) < 0 || + git_fs_path_to_dir(&worktree) < 0) { + error = -1; + goto cleanup; + } + + repo->workdir = git_str_detach(&worktree); + } else if (value) { + if (!*value) { + git_error_set(GIT_ERROR_NET, "working directory cannot be set to empty path"); + error = -1; + goto cleanup; + } + + if ((error = git_fs_path_prettify_dir(&worktree, + value, repo->gitdir)) < 0) + goto cleanup; + + repo->workdir = git_str_detach(&worktree); + } else if (parent_path && git_fs_path_isdir(parent_path->ptr)) { + repo->workdir = git_str_detach(parent_path); + } else { + if (git_fs_path_dirname_r(&worktree, repo->gitdir) < 0 || + git_fs_path_to_dir(&worktree) < 0) { + error = -1; + goto cleanup; + } + + repo->workdir = git_str_detach(&worktree); + } + + GIT_ERROR_CHECK_ALLOC(repo->workdir); + +cleanup: + git_str_dispose(&path); + git_str_dispose(&workdir_env); + git_config_entry_free(ce); + return error; +} + +/* + * This function returns furthest offset into path where a ceiling dir + * is found, so we can stop processing the path at that point. + * + * Note: converting this to use git_strs instead of GIT_PATH_MAX buffers on + * the stack could remove directories name limits, but at the cost of doing + * repeated malloc/frees inside the loop below, so let's not do it now. + */ +static size_t find_ceiling_dir_offset( + const char *path, + const char *ceiling_directories) +{ + char buf[GIT_PATH_MAX + 1]; + char buf2[GIT_PATH_MAX + 1]; + const char *ceil, *sep; + size_t len, max_len = 0, min_len; + + GIT_ASSERT_ARG(path); + + min_len = (size_t)(git_fs_path_root(path) + 1); + + if (ceiling_directories == NULL || min_len == 0) + return min_len; + + for (sep = ceil = ceiling_directories; *sep; ceil = sep + 1) { + for (sep = ceil; *sep && *sep != GIT_PATH_LIST_SEPARATOR; sep++); + len = sep - ceil; + + if (len == 0 || len >= sizeof(buf) || git_fs_path_root(ceil) == -1) + continue; + + strncpy(buf, ceil, len); + buf[len] = '\0'; + + if (p_realpath(buf, buf2) == NULL) + continue; + + len = strlen(buf2); + if (len > 0 && buf2[len-1] == '/') + buf[--len] = '\0'; + + if (!strncmp(path, buf2, len) && + (path[len] == '/' || !path[len]) && + len > max_len) + { + max_len = len; + } + } + + return (max_len <= min_len ? min_len : max_len); +} + +/* + * Read the contents of `file_path` and set `path_out` to the repo dir that + * it points to. Before calling, set `path_out` to the base directory that + * should be used if the contents of `file_path` are a relative path. + */ +static int read_gitfile(git_str *path_out, const char *file_path) +{ + int error = 0; + git_str file = GIT_STR_INIT; + size_t prefix_len = strlen(GIT_FILE_CONTENT_PREFIX); + + GIT_ASSERT_ARG(path_out); + GIT_ASSERT_ARG(file_path); + + if (git_futils_readbuffer(&file, file_path) < 0) + return -1; + + git_str_rtrim(&file); + /* apparently on Windows, some people use backslashes in paths */ + git_fs_path_mkposix(file.ptr); + + if (git_str_len(&file) <= prefix_len || + memcmp(git_str_cstr(&file), GIT_FILE_CONTENT_PREFIX, prefix_len) != 0) + { + git_error_set(GIT_ERROR_REPOSITORY, + "the `.git` file at '%s' is malformed", file_path); + error = -1; + } + else if ((error = git_fs_path_dirname_r(path_out, file_path)) >= 0) { + const char *gitlink = git_str_cstr(&file) + prefix_len; + while (*gitlink && git__isspace(*gitlink)) gitlink++; + + error = git_fs_path_prettify_dir( + path_out, gitlink, git_str_cstr(path_out)); + } + + git_str_dispose(&file); + return error; +} + +typedef struct { + const char *repo_path; + git_str tmp; + bool *is_safe; +} validate_ownership_data; + +static int validate_ownership_cb(const git_config_entry *entry, void *payload) +{ + validate_ownership_data *data = payload; + + if (strcmp(entry->value, "") == 0) { + *data->is_safe = false; + } else if (strcmp(entry->value, "*") == 0) { + *data->is_safe = true; + } else { + const char *test_path = entry->value; + +#ifdef GIT_WIN32 + /* + * Git for Windows does some truly bizarre things with + * paths that start with a forward slash; and expects you + * to escape that with `%(prefix)`. This syntax generally + * means to add the prefix that Git was installed to -- eg + * `/usr/local` -- unless it's an absolute path, in which + * case the leading `%(prefix)/` is just removed. And Git + * for Windows expects you to use this syntax for absolute + * Unix-style paths (in "Git Bash" or Windows Subsystem for + * Linux). + * + * Worse, the behavior used to be that a leading `/` was + * not absolute. It would indicate that Git for Windows + * should add the prefix. So `//` is required for absolute + * Unix-style paths. Yes, this is truly horrifying. + * + * Emulate that behavior, I guess, but only for absolute + * paths. We won't deal with the Git install prefix. Also, + * give WSL users an escape hatch where they don't have to + * think about this and can use the literal path that the + * filesystem APIs provide (`//wsl.localhost/...`). + */ + if (strncmp(test_path, "%(prefix)//", strlen("%(prefix)//")) == 0) + test_path += strlen("%(prefix)/"); + else if (strncmp(test_path, "//", 2) == 0 && + strncmp(test_path, "//wsl.localhost/", strlen("//wsl.localhost/")) != 0) + test_path++; +#endif + + if (git_fs_path_prettify_dir(&data->tmp, test_path, NULL) == 0 && + strcmp(data->tmp.ptr, data->repo_path) == 0) + *data->is_safe = true; + } + + return 0; +} + +static int validate_ownership_config( + bool *is_safe, + const char *path, + bool use_env) +{ + validate_ownership_data ownership_data = { + path, GIT_STR_INIT, is_safe + }; + git_config *config; + int error; + + if (load_global_config(&config, use_env) != 0) + return 0; + + error = git_config_get_multivar_foreach(config, + "safe.directory", NULL, + validate_ownership_cb, + &ownership_data); + + if (error == GIT_ENOTFOUND) + error = 0; + + git_config_free(config); + git_str_dispose(&ownership_data.tmp); + + return error; +} + +static int validate_ownership_path(bool *is_safe, const char *path) +{ + git_fs_path_owner_t owner_level = + GIT_FS_PATH_OWNER_CURRENT_USER | + GIT_FS_PATH_USER_IS_ADMINISTRATOR | + GIT_FS_PATH_OWNER_RUNNING_SUDO; + int error = 0; + + if (path) + error = git_fs_path_owner_is(is_safe, path, owner_level); + + if (error == GIT_ENOTFOUND) { + *is_safe = true; + error = 0; + } else if (error == GIT_EINVALID) { + *is_safe = false; + error = 0; + } + + return error; +} + +static int validate_ownership(git_repository *repo) +{ + const char *validation_paths[3] = { NULL }, *path; + size_t validation_len = 0, i; + bool is_safe = false; + int error = 0; + + /* + * If there's a worktree, validate the permissions to it *and* + * the git directory, and use the worktree as the configuration + * key for allowlisting the directory. In a bare setup, only + * look at the gitdir and use that as the allowlist. So we + * examine all `validation_paths` but use only the first as + * the configuration lookup. + */ + + if (repo->workdir) + validation_paths[validation_len++] = repo->workdir; + + if (repo->gitlink) + validation_paths[validation_len++] = repo->gitlink; + + validation_paths[validation_len++] = repo->gitdir; + + for (i = 0; i < validation_len; i++) { + path = validation_paths[i]; + + if ((error = validate_ownership_path(&is_safe, path)) < 0) + goto done; + + if (!is_safe) + break; + } + + if (is_safe || + (error = validate_ownership_config( + &is_safe, validation_paths[0], repo->use_env)) < 0) + goto done; + + if (!is_safe) { + git_error_set(GIT_ERROR_CONFIG, + "repository path '%s' is not owned by current user", + path); + error = GIT_EOWNER; + } + +done: + return error; +} + +struct repo_paths { + git_str gitdir; + git_str workdir; + git_str gitlink; + git_str commondir; +}; + +#define REPO_PATHS_INIT { GIT_STR_INIT } + +GIT_INLINE(void) repo_paths_dispose(struct repo_paths *paths) +{ + git_str_dispose(&paths->gitdir); + git_str_dispose(&paths->workdir); + git_str_dispose(&paths->gitlink); + git_str_dispose(&paths->commondir); +} + +static int find_repo_traverse( + struct repo_paths *out, + const char *start_path, + const char *ceiling_dirs, + uint32_t flags) +{ + git_str path = GIT_STR_INIT; + git_str repo_link = GIT_STR_INIT; + git_str common_link = GIT_STR_INIT; + struct stat st; + dev_t initial_device = 0; + int min_iterations; + bool in_dot_git, is_valid; + size_t ceiling_offset = 0; + int error; + + git_str_clear(&out->gitdir); + + if ((error = git_fs_path_prettify(&path, start_path, NULL)) < 0) + return error; + + /* + * In each loop we look first for a `.git` dir within the + * directory, then to see if the directory itself is a repo. + * + * In other words: if we start in /a/b/c, then we look at: + * /a/b/c/.git, /a/b/c, /a/b/.git, /a/b, /a/.git, /a + * + * With GIT_REPOSITORY_OPEN_BARE or GIT_REPOSITORY_OPEN_NO_DOTGIT, + * we assume we started with /a/b/c.git and don't append .git the + * first time through. min_iterations indicates the number of + * iterations left before going further counts as a search. + */ + if (flags & (GIT_REPOSITORY_OPEN_BARE | GIT_REPOSITORY_OPEN_NO_DOTGIT)) { + in_dot_git = true; + min_iterations = 1; + } else { + in_dot_git = false; + min_iterations = 2; + } + + for (;;) { + if (!(flags & GIT_REPOSITORY_OPEN_NO_DOTGIT)) { + if (!in_dot_git) { + if ((error = git_str_joinpath(&path, path.ptr, DOT_GIT)) < 0) + goto out; + } + in_dot_git = !in_dot_git; + } + + if (p_stat(path.ptr, &st) == 0) { + /* check that we have not crossed device boundaries */ + if (initial_device == 0) + initial_device = st.st_dev; + else if (st.st_dev != initial_device && + !(flags & GIT_REPOSITORY_OPEN_CROSS_FS)) + break; + + if (S_ISDIR(st.st_mode)) { + if ((error = is_valid_repository_path(&is_valid, &path, &common_link, flags)) < 0) + goto out; + + if (is_valid) { + if ((error = git_fs_path_to_dir(&path)) < 0 || + (error = git_str_set(&out->gitdir, path.ptr, path.size)) < 0) + goto out; + + if ((error = git_str_attach(&out->gitlink, git_worktree__read_link(path.ptr, GIT_GITDIR_FILE), 0)) < 0) + goto out; + + git_str_swap(&common_link, &out->commondir); + + break; + } + } else if (S_ISREG(st.st_mode) && git__suffixcmp(path.ptr, "/" DOT_GIT) == 0) { + if ((error = read_gitfile(&repo_link, path.ptr)) < 0 || + (error = is_valid_repository_path(&is_valid, &repo_link, &common_link, flags)) < 0) + goto out; + + if (is_valid) { + git_str_swap(&out->gitdir, &repo_link); + + if ((error = git_str_put(&out->gitlink, path.ptr, path.size)) < 0) + goto out; + + git_str_swap(&common_link, &out->commondir); + } + break; + } + } + + /* + * Move up one directory. If we're in_dot_git, we'll + * search the parent itself next. If we're !in_dot_git, + * we'll search .git in the parent directory next (added + * at the top of the loop). + */ + if ((error = git_fs_path_dirname_r(&path, path.ptr)) < 0) + goto out; + + /* + * Once we've checked the directory (and .git if + * applicable), find the ceiling for a search. + */ + if (min_iterations && (--min_iterations == 0)) + ceiling_offset = find_ceiling_dir_offset(path.ptr, ceiling_dirs); + + /* Check if we should stop searching here. */ + if (min_iterations == 0 && + (path.ptr[ceiling_offset] == 0 || (flags & GIT_REPOSITORY_OPEN_NO_SEARCH))) + break; + } + + if (!(flags & GIT_REPOSITORY_OPEN_BARE)) { + if (!git_str_len(&out->gitdir)) + git_str_clear(&out->workdir); + else if ((error = git_fs_path_dirname_r(&out->workdir, path.ptr)) < 0 || + (error = git_fs_path_to_dir(&out->workdir)) < 0) + goto out; + } + + /* If we didn't find the repository, and we don't have any other + * error to report, report that. */ + if (!git_str_len(&out->gitdir)) { + git_error_set(GIT_ERROR_REPOSITORY, "could not find repository at '%s'", start_path); + error = GIT_ENOTFOUND; + goto out; + } + +out: + if (error) + repo_paths_dispose(out); + + git_str_dispose(&path); + git_str_dispose(&repo_link); + git_str_dispose(&common_link); + return error; +} + +static int load_grafts(git_repository *repo) +{ + git_str path = GIT_STR_INIT; + int error; + + if ((error = git_repository__item_path(&path, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 || + (error = git_str_joinpath(&path, path.ptr, "grafts")) < 0 || + (error = git_grafts_open_or_refresh(&repo->grafts, path.ptr, repo->oid_type)) < 0) + goto error; + + git_str_clear(&path); + + if ((error = git_str_joinpath(&path, repo->gitdir, "shallow")) < 0 || + (error = git_grafts_open_or_refresh(&repo->shallow_grafts, path.ptr, repo->oid_type)) < 0) + goto error; + +error: + git_str_dispose(&path); + return error; +} + +static int find_repo( + struct repo_paths *out, + const char *start_path, + const char *ceiling_dirs, + uint32_t flags) +{ + bool use_env = !!(flags & GIT_REPOSITORY_OPEN_FROM_ENV); + git_str gitdir_buf = GIT_STR_INIT, + ceiling_dirs_buf = GIT_STR_INIT, + across_fs_buf = GIT_STR_INIT; + int error; + + if (use_env && !start_path) { + error = git__getenv(&gitdir_buf, "GIT_DIR"); + + if (!error) { + start_path = gitdir_buf.ptr; + flags |= GIT_REPOSITORY_OPEN_NO_SEARCH; + flags |= GIT_REPOSITORY_OPEN_NO_DOTGIT; + } else if (error == GIT_ENOTFOUND) { + start_path = "."; + } else { + goto done; + } + } + + if (use_env && !ceiling_dirs) { + error = git__getenv(&ceiling_dirs_buf, + "GIT_CEILING_DIRECTORIES"); + + if (!error) + ceiling_dirs = ceiling_dirs_buf.ptr; + else if (error != GIT_ENOTFOUND) + goto done; + } + + if (use_env) { + error = git__getenv(&across_fs_buf, + "GIT_DISCOVERY_ACROSS_FILESYSTEM"); + + if (!error) { + int across_fs = 0; + + if ((error = git_config_parse_bool(&across_fs, + git_str_cstr(&across_fs_buf))) < 0) + goto done; + + if (across_fs) + flags |= GIT_REPOSITORY_OPEN_CROSS_FS; + } else if (error != GIT_ENOTFOUND) { + goto done; + } + } + + error = find_repo_traverse(out, start_path, ceiling_dirs, flags); + +done: + git_str_dispose(&gitdir_buf); + git_str_dispose(&ceiling_dirs_buf); + git_str_dispose(&across_fs_buf); + + return error; +} + +static int obtain_config_and_set_oid_type( + git_config **config_ptr, + git_repository *repo) +{ + int error; + git_config *config = NULL; + int version = 0; + + /* + * We'd like to have the config, but git doesn't particularly + * care if it's not there, so we need to deal with that. + */ + + error = git_repository_config_snapshot(&config, repo); + if (error < 0 && error != GIT_ENOTFOUND) + goto out; + + if (config && + (error = check_repositoryformatversion(&version, config)) < 0) + goto out; + + if ((error = check_extensions(config, version)) < 0) + goto out; + + if (version > 0) { + if ((error = load_objectformat(repo, config)) < 0) + goto out; + } else { + repo->oid_type = GIT_OID_DEFAULT; + } + +out: + *config_ptr = config; + + return error; +} + +int git_repository_open_bare( + git_repository **repo_ptr, + const char *bare_path) +{ + git_str path = GIT_STR_INIT, common_path = GIT_STR_INIT; + git_repository *repo = NULL; + bool is_valid; + int error; + git_config *config; + + if ((error = git_fs_path_prettify_dir(&path, bare_path, NULL)) < 0 || + (error = is_valid_repository_path(&is_valid, &path, &common_path, 0)) < 0) + return error; + + if (!is_valid) { + git_str_dispose(&path); + git_str_dispose(&common_path); + git_error_set(GIT_ERROR_REPOSITORY, "path is not a repository: %s", bare_path); + return GIT_ENOTFOUND; + } + + repo = repository_alloc(); + GIT_ERROR_CHECK_ALLOC(repo); + + repo->gitdir = git_str_detach(&path); + GIT_ERROR_CHECK_ALLOC(repo->gitdir); + repo->commondir = git_str_detach(&common_path); + GIT_ERROR_CHECK_ALLOC(repo->commondir); + + /* of course we're bare! */ + repo->is_bare = 1; + repo->is_worktree = 0; + repo->workdir = NULL; + + if ((error = obtain_config_and_set_oid_type(&config, repo)) < 0) + goto cleanup; + + *repo_ptr = repo; + +cleanup: + git_config_free(config); + + return error; +} + +static int repo_load_namespace(git_repository *repo) +{ + git_str namespace_buf = GIT_STR_INIT; + int error; + + if (!repo->use_env) + return 0; + + error = git__getenv(&namespace_buf, "GIT_NAMESPACE"); + + if (error == 0) + repo->namespace = git_str_detach(&namespace_buf); + else if (error != GIT_ENOTFOUND) + return error; + + return 0; +} + +static int repo_is_worktree(unsigned *out, const git_repository *repo) +{ + git_str gitdir_link = GIT_STR_INIT; + int error; + + /* Worktrees cannot have the same commondir and gitdir */ + if (repo->commondir && repo->gitdir + && !strcmp(repo->commondir, repo->gitdir)) { + *out = 0; + return 0; + } + + if ((error = git_str_joinpath(&gitdir_link, repo->gitdir, "gitdir")) < 0) + return -1; + + /* A 'gitdir' file inside a git directory is currently + * only used when the repository is a working tree. */ + *out = !!git_fs_path_exists(gitdir_link.ptr); + + git_str_dispose(&gitdir_link); + return error; +} + +int git_repository_open_ext( + git_repository **repo_ptr, + const char *start_path, + unsigned int flags, + const char *ceiling_dirs) +{ + struct repo_paths paths = { GIT_STR_INIT }; + git_repository *repo = NULL; + git_config *config = NULL; + unsigned is_worktree; + int error; + + if (repo_ptr) + *repo_ptr = NULL; + + error = find_repo(&paths, start_path, ceiling_dirs, flags); + + if (error < 0 || !repo_ptr) + goto cleanup; + + repo = repository_alloc(); + GIT_ERROR_CHECK_ALLOC(repo); + + repo->use_env = !!(flags & GIT_REPOSITORY_OPEN_FROM_ENV); + + repo->gitdir = git_str_detach(&paths.gitdir); + GIT_ERROR_CHECK_ALLOC(repo->gitdir); + + if (paths.gitlink.size) { + repo->gitlink = git_str_detach(&paths.gitlink); + GIT_ERROR_CHECK_ALLOC(repo->gitlink); + } + if (paths.commondir.size) { + repo->commondir = git_str_detach(&paths.commondir); + GIT_ERROR_CHECK_ALLOC(repo->commondir); + } + + if ((error = repo_is_worktree(&is_worktree, repo)) < 0) + goto cleanup; + + repo->is_worktree = is_worktree; + + error = obtain_config_and_set_oid_type(&config, repo); + if (error < 0) + goto cleanup; + + if ((error = load_grafts(repo)) < 0) + goto cleanup; + + if ((flags & GIT_REPOSITORY_OPEN_BARE) != 0) { + repo->is_bare = 1; + } else { + if (config && + ((error = load_config_data(repo, config)) < 0 || + (error = load_workdir(repo, config, &paths.workdir)) < 0)) + goto cleanup; + } + + if ((error = repo_load_namespace(repo)) < 0) + goto cleanup; + + /* + * Ensure that the git directory and worktree are + * owned by the current user. + */ + if (git_repository__validate_ownership && + (error = validate_ownership(repo)) < 0) + goto cleanup; + +cleanup: + repo_paths_dispose(&paths); + git_config_free(config); + + if (error < 0) + git_repository_free(repo); + else if (repo_ptr) + *repo_ptr = repo; + + return error; +} + +int git_repository_open(git_repository **repo_out, const char *path) +{ + return git_repository_open_ext( + repo_out, path, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL); +} + +int git_repository_open_from_worktree(git_repository **repo_out, git_worktree *wt) +{ + git_str path = GIT_STR_INIT; + git_repository *repo = NULL; + size_t len; + int err; + + GIT_ASSERT_ARG(repo_out); + GIT_ASSERT_ARG(wt); + + *repo_out = NULL; + len = strlen(wt->gitlink_path); + + if (len <= 4 || strcasecmp(wt->gitlink_path + len - 4, ".git")) { + err = -1; + goto out; + } + + if ((err = git_str_set(&path, wt->gitlink_path, len - 4)) < 0) + goto out; + + if ((err = git_repository_open(&repo, path.ptr)) < 0) + goto out; + + *repo_out = repo; + +out: + git_str_dispose(&path); + + return err; +} + +int git_repository__wrap_odb( + git_repository **out, + git_odb *odb, + git_oid_t oid_type) +{ + git_repository *repo; + + repo = repository_alloc(); + GIT_ERROR_CHECK_ALLOC(repo); + + repo->oid_type = oid_type; + + git_repository_set_odb(repo, odb); + *out = repo; + + return 0; +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +int git_repository_wrap_odb( + git_repository **out, + git_odb *odb, + git_oid_t oid_type) +{ + return git_repository__wrap_odb(out, odb, oid_type); +} +#else +int git_repository_wrap_odb(git_repository **out, git_odb *odb) +{ + return git_repository__wrap_odb(out, odb, GIT_OID_DEFAULT); +} +#endif + +int git_repository_discover( + git_buf *out, + const char *start_path, + int across_fs, + const char *ceiling_dirs) +{ + struct repo_paths paths = { GIT_STR_INIT }; + uint32_t flags = across_fs ? GIT_REPOSITORY_OPEN_CROSS_FS : 0; + int error; + + GIT_ASSERT_ARG(start_path); + + if ((error = find_repo(&paths, start_path, ceiling_dirs, flags)) == 0) + error = git_buf_fromstr(out, &paths.gitdir); + + repo_paths_dispose(&paths); + return error; +} + +static int load_config( + git_config **out, + git_repository *repo, + const char *global_config_path, + const char *xdg_config_path, + const char *system_config_path, + const char *programdata_path) +{ + int error; + git_str config_path = GIT_STR_INIT; + git_config *cfg = NULL; + + GIT_ASSERT_ARG(out); + + if ((error = git_config_new(&cfg)) < 0) + return error; + + if (repo) { + if ((error = git_repository__item_path(&config_path, repo, GIT_REPOSITORY_ITEM_CONFIG)) == 0) + error = git_config_add_file_ondisk(cfg, config_path.ptr, GIT_CONFIG_LEVEL_LOCAL, repo, 0); + + if (error && error != GIT_ENOTFOUND) + goto on_error; + + git_str_dispose(&config_path); + } + + if (global_config_path != NULL && + (error = git_config_add_file_ondisk( + cfg, global_config_path, GIT_CONFIG_LEVEL_GLOBAL, repo, 0)) < 0 && + error != GIT_ENOTFOUND) + goto on_error; + + if (xdg_config_path != NULL && + (error = git_config_add_file_ondisk( + cfg, xdg_config_path, GIT_CONFIG_LEVEL_XDG, repo, 0)) < 0 && + error != GIT_ENOTFOUND) + goto on_error; + + if (system_config_path != NULL && + (error = git_config_add_file_ondisk( + cfg, system_config_path, GIT_CONFIG_LEVEL_SYSTEM, repo, 0)) < 0 && + error != GIT_ENOTFOUND) + goto on_error; + + if (programdata_path != NULL && + (error = git_config_add_file_ondisk( + cfg, programdata_path, GIT_CONFIG_LEVEL_PROGRAMDATA, repo, 0)) < 0 && + error != GIT_ENOTFOUND) + goto on_error; + + git_error_clear(); /* clear any lingering ENOTFOUND errors */ + + *out = cfg; + return 0; + +on_error: + git_str_dispose(&config_path); + git_config_free(cfg); + *out = NULL; + return error; +} + +static const char *path_unless_empty(git_str *buf) +{ + return git_str_len(buf) > 0 ? git_str_cstr(buf) : NULL; +} + +GIT_INLINE(int) config_path_system(git_str *out, bool use_env) +{ + if (use_env) { + git_str no_system_buf = GIT_STR_INIT; + int no_system = 0; + int error; + + error = git__getenv(&no_system_buf, "GIT_CONFIG_NOSYSTEM"); + + if (error && error != GIT_ENOTFOUND) + return error; + + error = git_config_parse_bool(&no_system, no_system_buf.ptr); + git_str_dispose(&no_system_buf); + + if (no_system) + return 0; + + error = git__getenv(out, "GIT_CONFIG_SYSTEM"); + + if (error == 0 || error != GIT_ENOTFOUND) + return 0; + } + + git_config__find_system(out); + return 0; +} + +GIT_INLINE(int) config_path_global(git_str *out, bool use_env) +{ + if (use_env) { + int error = git__getenv(out, "GIT_CONFIG_GLOBAL"); + + if (error == 0 || error != GIT_ENOTFOUND) + return 0; + } + + git_config__find_global(out); + return 0; +} + +int git_repository_config__weakptr(git_config **out, git_repository *repo) +{ + int error = 0; + + if (repo->_config == NULL) { + git_str system_buf = GIT_STR_INIT; + git_str global_buf = GIT_STR_INIT; + git_str xdg_buf = GIT_STR_INIT; + git_str programdata_buf = GIT_STR_INIT; + bool use_env = repo->use_env; + git_config *config; + + if (!(error = config_path_system(&system_buf, use_env)) && + !(error = config_path_global(&global_buf, use_env))) { + git_config__find_xdg(&xdg_buf); + git_config__find_programdata(&programdata_buf); + } + + if (!error) { + /* + * If there is no global file, open a backend + * for it anyway. + */ + if (git_str_len(&global_buf) == 0) + git_config__global_location(&global_buf); + + error = load_config( + &config, repo, + path_unless_empty(&global_buf), + path_unless_empty(&xdg_buf), + path_unless_empty(&system_buf), + path_unless_empty(&programdata_buf)); + } + + if (!error) { + GIT_REFCOUNT_OWN(config, repo); + + if (git_atomic_compare_and_swap(&repo->_config, NULL, config) != NULL) { + GIT_REFCOUNT_OWN(config, NULL); + git_config_free(config); + } + } + + git_str_dispose(&global_buf); + git_str_dispose(&xdg_buf); + git_str_dispose(&system_buf); + git_str_dispose(&programdata_buf); + } + + *out = repo->_config; + return error; +} + +int git_repository_config(git_config **out, git_repository *repo) +{ + if (git_repository_config__weakptr(out, repo) < 0) + return -1; + + GIT_REFCOUNT_INC(*out); + return 0; +} + +int git_repository_config_snapshot(git_config **out, git_repository *repo) +{ + int error; + git_config *weak; + + if ((error = git_repository_config__weakptr(&weak, repo)) < 0) + return error; + + return git_config_snapshot(out, weak); +} + +int git_repository_set_config(git_repository *repo, git_config *config) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(config); + + set_config(repo, config); + return 0; +} + +static int repository_odb_path(git_str *out, git_repository *repo) +{ + int error = GIT_ENOTFOUND; + + if (repo->use_env) + error = git__getenv(out, "GIT_OBJECT_DIRECTORY"); + + if (error == GIT_ENOTFOUND) + error = git_repository__item_path(out, repo, + GIT_REPOSITORY_ITEM_OBJECTS); + + return error; +} + +static int repository_odb_alternates( + git_odb *odb, + git_repository *repo) +{ + git_str alternates = GIT_STR_INIT; + char *sep, *alt; + int error; + + if (!repo->use_env) + return 0; + + error = git__getenv(&alternates, "GIT_ALTERNATE_OBJECT_DIRECTORIES"); + + if (error != 0) + return (error == GIT_ENOTFOUND) ? 0 : error; + + alt = alternates.ptr; + + while (*alt) { + sep = strchr(alt, GIT_PATH_LIST_SEPARATOR); + + if (sep) + *sep = '\0'; + + error = git_odb_add_disk_alternate(odb, alt); + + if (sep) + alt = sep + 1; + else + break; + } + + git_str_dispose(&alternates); + return 0; +} + +int git_repository_odb__weakptr(git_odb **out, git_repository *repo) +{ + int error = 0; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(out); + + *out = git_atomic_load(repo->_odb); + if (*out == NULL) { + git_str odb_path = GIT_STR_INIT; + git_odb_options odb_opts = GIT_ODB_OPTIONS_INIT; + git_odb *odb; + + odb_opts.oid_type = repo->oid_type; + + if ((error = repository_odb_path(&odb_path, repo)) < 0 || + (error = git_odb__new(&odb, &odb_opts)) < 0 || + (error = repository_odb_alternates(odb, repo)) < 0) + return error; + + GIT_REFCOUNT_OWN(odb, repo); + + if ((error = git_odb__set_caps(odb, GIT_ODB_CAP_FROM_OWNER)) < 0 || + (error = git_odb__add_default_backends(odb, odb_path.ptr, 0, 0)) < 0) { + git_odb_free(odb); + return error; + } + + if (git_atomic_compare_and_swap(&repo->_odb, NULL, odb) != NULL) { + GIT_REFCOUNT_OWN(odb, NULL); + git_odb_free(odb); + } + + git_str_dispose(&odb_path); + *out = git_atomic_load(repo->_odb); + } + + return error; +} + +int git_repository_odb(git_odb **out, git_repository *repo) +{ + if (git_repository_odb__weakptr(out, repo) < 0) + return -1; + + GIT_REFCOUNT_INC(*out); + return 0; +} + +int git_repository_set_odb(git_repository *repo, git_odb *odb) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(odb); + + set_odb(repo, odb); + return 0; +} + +int git_repository_refdb__weakptr(git_refdb **out, git_repository *repo) +{ + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + if (repo->_refdb == NULL) { + git_refdb *refdb; + + error = git_refdb_open(&refdb, repo); + if (!error) { + GIT_REFCOUNT_OWN(refdb, repo); + + if (git_atomic_compare_and_swap(&repo->_refdb, NULL, refdb) != NULL) { + GIT_REFCOUNT_OWN(refdb, NULL); + git_refdb_free(refdb); + } + } + } + + *out = repo->_refdb; + return error; +} + +int git_repository_refdb(git_refdb **out, git_repository *repo) +{ + if (git_repository_refdb__weakptr(out, repo) < 0) + return -1; + + GIT_REFCOUNT_INC(*out); + return 0; +} + +int git_repository_set_refdb(git_repository *repo, git_refdb *refdb) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(refdb); + + set_refdb(repo, refdb); + return 0; +} + +static int repository_index_path(git_str *out, git_repository *repo) +{ + int error = GIT_ENOTFOUND; + + if (repo->use_env) + error = git__getenv(out, "GIT_INDEX_FILE"); + + if (error == GIT_ENOTFOUND) + error = git_repository__item_path(out, repo, + GIT_REPOSITORY_ITEM_INDEX); + + return error; +} + +int git_repository_index__weakptr(git_index **out, git_repository *repo) +{ + int error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + if (repo->_index == NULL) { + git_str index_path = GIT_STR_INIT; + git_index *index; + + if ((error = repository_index_path(&index_path, repo)) < 0) + return error; + + error = git_index__open(&index, index_path.ptr, repo->oid_type); + + if (!error) { + GIT_REFCOUNT_OWN(index, repo); + + if (git_atomic_compare_and_swap(&repo->_index, NULL, index) != NULL) { + GIT_REFCOUNT_OWN(index, NULL); + git_index_free(index); + } + + error = git_index_set_caps(repo->_index, + GIT_INDEX_CAPABILITY_FROM_OWNER); + } + + git_str_dispose(&index_path); + } + + *out = repo->_index; + return error; +} + +int git_repository_index(git_index **out, git_repository *repo) +{ + if (git_repository_index__weakptr(out, repo) < 0) + return -1; + + GIT_REFCOUNT_INC(*out); + return 0; +} + +int git_repository_set_index(git_repository *repo, git_index *index) +{ + GIT_ASSERT_ARG(repo); + set_index(repo, index); + return 0; +} + +int git_repository_grafts__weakptr(git_grafts **out, git_repository *repo) +{ + GIT_ASSERT_ARG(out && repo); + GIT_ASSERT(repo->grafts); + *out = repo->grafts; + return 0; +} + +int git_repository_shallow_grafts__weakptr(git_grafts **out, git_repository *repo) +{ + GIT_ASSERT_ARG(out && repo); + GIT_ASSERT(repo->shallow_grafts); + *out = repo->shallow_grafts; + return 0; +} + +int git_repository_set_namespace(git_repository *repo, const char *namespace) +{ + git__free(repo->namespace); + + if (namespace == NULL) { + repo->namespace = NULL; + return 0; + } + + return (repo->namespace = git__strdup(namespace)) ? 0 : -1; +} + +const char *git_repository_get_namespace(git_repository *repo) +{ + return repo->namespace; +} + +#ifdef GIT_WIN32 +static int reserved_names_add8dot3(git_repository *repo, const char *path) +{ + char *name = git_win32_path_8dot3_name(path); + const char *def = GIT_DIR_SHORTNAME; + const char *def_dot_git = DOT_GIT; + size_t name_len, def_len = CONST_STRLEN(GIT_DIR_SHORTNAME); + size_t def_dot_git_len = CONST_STRLEN(DOT_GIT); + git_str *buf; + + if (!name) + return 0; + + name_len = strlen(name); + + if ((name_len == def_len && memcmp(name, def, def_len) == 0) || + (name_len == def_dot_git_len && memcmp(name, def_dot_git, def_dot_git_len) == 0)) { + git__free(name); + return 0; + } + + if ((buf = git_array_alloc(repo->reserved_names)) == NULL) + return -1; + + git_str_attach(buf, name, name_len); + return true; +} + +bool git_repository__reserved_names( + git_str **out, size_t *outlen, git_repository *repo, bool include_ntfs) +{ + GIT_UNUSED(include_ntfs); + + if (repo->reserved_names.size == 0) { + git_str *buf; + size_t i; + + /* Add the static defaults */ + for (i = 0; i < git_repository__reserved_names_win32_len; i++) { + if ((buf = git_array_alloc(repo->reserved_names)) == NULL) + goto on_error; + + buf->ptr = git_repository__reserved_names_win32[i].ptr; + buf->size = git_repository__reserved_names_win32[i].size; + } + + /* Try to add any repo-specific reserved names - the gitlink file + * within a submodule or the repository (if the repository directory + * is beneath the workdir). These are typically `.git`, but should + * be protected in case they are not. Note, repo and workdir paths + * are always prettified to end in `/`, so a prefixcmp is safe. + */ + if (!repo->is_bare) { + int (*prefixcmp)(const char *, const char *); + int error, ignorecase; + + error = git_repository__configmap_lookup( + &ignorecase, repo, GIT_CONFIGMAP_IGNORECASE); + prefixcmp = (error || ignorecase) ? git__prefixcmp_icase : + git__prefixcmp; + + if (repo->gitlink && + reserved_names_add8dot3(repo, repo->gitlink) < 0) + goto on_error; + + if (repo->gitdir && + prefixcmp(repo->gitdir, repo->workdir) == 0 && + reserved_names_add8dot3(repo, repo->gitdir) < 0) + goto on_error; + } + } + + *out = repo->reserved_names.ptr; + *outlen = repo->reserved_names.size; + + return true; + + /* Always give good defaults, even on OOM */ +on_error: + *out = git_repository__reserved_names_win32; + *outlen = git_repository__reserved_names_win32_len; + + return false; +} +#else +bool git_repository__reserved_names( + git_str **out, size_t *outlen, git_repository *repo, bool include_ntfs) +{ + GIT_UNUSED(repo); + + if (include_ntfs) { + *out = git_repository__reserved_names_win32; + *outlen = git_repository__reserved_names_win32_len; + } else { + *out = git_repository__reserved_names_posix; + *outlen = git_repository__reserved_names_posix_len; + } + + return true; +} +#endif + +static int check_repositoryformatversion(int *version, git_config *config) +{ + int error; + + error = git_config_get_int32(version, config, "core.repositoryformatversion"); + + /* git ignores this if the config variable isn't there */ + if (error == GIT_ENOTFOUND) + return 0; + + if (error < 0) + return -1; + + if (*version < 0) { + git_error_set(GIT_ERROR_REPOSITORY, + "invalid repository version %d", *version); + } + + if (GIT_REPO_VERSION_MAX < *version) { + git_error_set(GIT_ERROR_REPOSITORY, + "unsupported repository version %d; only versions up to %d are supported", + *version, GIT_REPO_VERSION_MAX); + return -1; + } + + return 0; +} + +static const char *builtin_extensions[] = { + "noop", + "objectformat" +}; + +static git_vector user_extensions = { 0, git__strcmp_cb }; + +static int check_valid_extension(const git_config_entry *entry, void *payload) +{ + git_str cfg = GIT_STR_INIT; + bool reject; + const char *extension; + size_t i; + int error = 0; + + GIT_UNUSED(payload); + + git_vector_foreach (&user_extensions, i, extension) { + git_str_clear(&cfg); + + /* + * Users can specify that they don't want to support an + * extension with a '!' prefix. + */ + if ((reject = (extension[0] == '!')) == true) + extension = &extension[1]; + + if ((error = git_str_printf(&cfg, "extensions.%s", extension)) < 0) + goto done; + + if (strcmp(entry->name, cfg.ptr) == 0) { + if (reject) + goto fail; + + goto done; + } + } + + for (i = 0; i < ARRAY_SIZE(builtin_extensions); i++) { + git_str_clear(&cfg); + extension = builtin_extensions[i]; + + if ((error = git_str_printf(&cfg, "extensions.%s", extension)) < 0) + goto done; + + if (strcmp(entry->name, cfg.ptr) == 0) + goto done; + } + +fail: + git_error_set(GIT_ERROR_REPOSITORY, "unsupported extension name %s", entry->name); + error = -1; + +done: + git_str_dispose(&cfg); + return error; +} + +static int check_extensions(git_config *config, int version) +{ + if (version < 1) + return 0; + + return git_config_foreach_match(config, "^extensions\\.", check_valid_extension, NULL); +} + +static int load_objectformat(git_repository *repo, git_config *config) +{ + git_config_entry *entry = NULL; + int error; + + if ((error = git_config_get_entry(&entry, config, "extensions.objectformat")) < 0) { + if (error == GIT_ENOTFOUND) { + repo->oid_type = GIT_OID_DEFAULT; + git_error_clear(); + error = 0; + } + + goto done; + } + + if ((repo->oid_type = git_oid_type_fromstr(entry->value)) == 0) { + git_error_set(GIT_ERROR_REPOSITORY, + "unknown object format '%s'", entry->value); + error = GIT_EINVALID; + } + +done: + git_config_entry_free(entry); + return error; +} + +int git_repository__set_objectformat( + git_repository *repo, + git_oid_t oid_type) +{ + git_config *cfg; + + /* + * Older clients do not necessarily understand the + * `objectformat` extension, even when it's set to an + * object format that they understand (SHA1). Do not set + * the objectformat extension unless we're not using the + * default object format. + */ + if (oid_type == GIT_OID_DEFAULT) + return 0; + + if (!git_repository_is_empty(repo) && repo->oid_type != oid_type) { + git_error_set(GIT_ERROR_REPOSITORY, + "cannot change object id type of existing repository"); + return -1; + } + + if (git_repository_config__weakptr(&cfg, repo) < 0) + return -1; + + if (git_config_set_int32(cfg, + "core.repositoryformatversion", 1) < 0 || + git_config_set_string(cfg, "extensions.objectformat", + git_oid_type_name(oid_type)) < 0) + return -1; + + /* + * During repo init, we may create some backends with the + * default oid type. Clear them so that we create them with + * the proper oid type. + */ + if (repo->oid_type != oid_type) { + set_index(repo, NULL); + set_odb(repo, NULL); + set_refdb(repo, NULL); + + repo->oid_type = oid_type; + } + + return 0; +} + +int git_repository__extensions(char ***out, size_t *out_len) +{ + git_vector extensions; + const char *builtin, *user; + char *extension; + size_t i, j; + + if (git_vector_init(&extensions, 8, git__strcmp_cb) < 0) + return -1; + + for (i = 0; i < ARRAY_SIZE(builtin_extensions); i++) { + bool match = false; + + builtin = builtin_extensions[i]; + + git_vector_foreach (&user_extensions, j, user) { + if (user[0] == '!' && strcmp(builtin, &user[1]) == 0) { + match = true; + break; + } + } + + if (match) + continue; + + if ((extension = git__strdup(builtin)) == NULL || + git_vector_insert(&extensions, extension) < 0) + return -1; + } + + git_vector_foreach (&user_extensions, i, user) { + if (user[0] == '!') + continue; + + if ((extension = git__strdup(user)) == NULL || + git_vector_insert(&extensions, extension) < 0) + return -1; + } + + git_vector_sort(&extensions); + + *out = (char **)git_vector_detach(out_len, NULL, &extensions); + return 0; +} + +static int dup_ext_err(void **old, void *extension) +{ + GIT_UNUSED(old); + GIT_UNUSED(extension); + return GIT_EEXISTS; +} + +int git_repository__set_extensions(const char **extensions, size_t len) +{ + char *extension; + size_t i, j; + int error; + + git_repository__free_extensions(); + + for (i = 0; i < len; i++) { + bool is_builtin = false; + + for (j = 0; j < ARRAY_SIZE(builtin_extensions); j++) { + if (strcmp(builtin_extensions[j], extensions[i]) == 0) { + is_builtin = true; + break; + } + } + + if (is_builtin) + continue; + + if ((extension = git__strdup(extensions[i])) == NULL) + return -1; + + if ((error = git_vector_insert_sorted(&user_extensions, extension, dup_ext_err)) < 0) { + git__free(extension); + + if (error != GIT_EEXISTS) + return -1; + } + } + + return 0; +} + +void git_repository__free_extensions(void) +{ + git_vector_free_deep(&user_extensions); +} + +int git_repository_create_head(const char *git_dir, const char *ref_name) +{ + git_str ref_path = GIT_STR_INIT; + git_filebuf ref = GIT_FILEBUF_INIT; + const char *fmt; + int error; + + if ((error = git_str_joinpath(&ref_path, git_dir, GIT_HEAD_FILE)) < 0 || + (error = git_filebuf_open(&ref, ref_path.ptr, 0, GIT_REFS_FILE_MODE)) < 0) + goto out; + + if (git__prefixcmp(ref_name, GIT_REFS_DIR) == 0) + fmt = "ref: %s\n"; + else + fmt = "ref: " GIT_REFS_HEADS_DIR "%s\n"; + + if ((error = git_filebuf_printf(&ref, fmt, ref_name)) < 0 || + (error = git_filebuf_commit(&ref)) < 0) + goto out; + +out: + git_str_dispose(&ref_path); + git_filebuf_cleanup(&ref); + return error; +} + +static bool is_chmod_supported(const char *file_path) +{ + struct stat st1, st2; + + if (p_stat(file_path, &st1) < 0) + return false; + + if (p_chmod(file_path, st1.st_mode ^ S_IXUSR) < 0) + return false; + + if (p_stat(file_path, &st2) < 0) + return false; + + return (st1.st_mode != st2.st_mode); +} + +static bool is_filesystem_case_insensitive(const char *gitdir_path) +{ + git_str path = GIT_STR_INIT; + int is_insensitive = -1; + + if (!git_str_joinpath(&path, gitdir_path, "CoNfIg")) + is_insensitive = git_fs_path_exists(git_str_cstr(&path)); + + git_str_dispose(&path); + return is_insensitive; +} + +/* + * Return a configuration object with only the global and system + * configurations; no repository-level configuration. + */ +static int load_global_config(git_config **config, bool use_env) +{ + git_str global_buf = GIT_STR_INIT; + git_str xdg_buf = GIT_STR_INIT; + git_str system_buf = GIT_STR_INIT; + git_str programdata_buf = GIT_STR_INIT; + int error; + + if (!(error = config_path_system(&system_buf, use_env)) && + !(error = config_path_global(&global_buf, use_env))) { + git_config__find_xdg(&xdg_buf); + git_config__find_programdata(&programdata_buf); + + error = load_config(config, NULL, + path_unless_empty(&global_buf), + path_unless_empty(&xdg_buf), + path_unless_empty(&system_buf), + path_unless_empty(&programdata_buf)); + } + + git_str_dispose(&global_buf); + git_str_dispose(&xdg_buf); + git_str_dispose(&system_buf); + git_str_dispose(&programdata_buf); + + return error; +} + +static bool are_symlinks_supported(const char *wd_path, bool use_env) +{ + git_config *config = NULL; + int symlinks = 0; + + /* + * To emulate Git for Windows, symlinks on Windows must be explicitly + * opted-in. We examine the system configuration for a core.symlinks + * set to true. If found, we then examine the filesystem to see if + * symlinks are _actually_ supported by the current user. If that is + * _not_ set, then we do not test or enable symlink support. + */ +#ifdef GIT_WIN32 + if (load_global_config(&config, use_env) < 0 || + git_config_get_bool(&symlinks, config, "core.symlinks") < 0 || + !symlinks) + goto done; +#else + GIT_UNUSED(use_env); +#endif + + if (!(symlinks = git_fs_path_supports_symlinks(wd_path))) + goto done; + +done: + git_config_free(config); + return symlinks != 0; +} + +static int create_empty_file(const char *path, mode_t mode) +{ + int fd; + + if ((fd = p_creat(path, mode)) < 0) { + git_error_set(GIT_ERROR_OS, "error while creating '%s'", path); + return -1; + } + + if (p_close(fd) < 0) { + git_error_set(GIT_ERROR_OS, "error while closing '%s'", path); + return -1; + } + + return 0; +} + +static int repo_local_config( + git_config **out, + git_str *config_dir, + git_repository *repo, + const char *repo_dir) +{ + int error = 0; + git_config *parent; + const char *cfg_path; + + if (git_str_joinpath(config_dir, repo_dir, GIT_CONFIG_FILENAME_INREPO) < 0) + return -1; + cfg_path = git_str_cstr(config_dir); + + /* make LOCAL config if missing */ + if (!git_fs_path_isfile(cfg_path) && + (error = create_empty_file(cfg_path, GIT_CONFIG_FILE_MODE)) < 0) + return error; + + /* if no repo, just open that file directly */ + if (!repo) + return git_config_open_ondisk(out, cfg_path); + + /* otherwise, open parent config and get that level */ + if ((error = git_repository_config__weakptr(&parent, repo)) < 0) + return error; + + if (git_config_open_level(out, parent, GIT_CONFIG_LEVEL_LOCAL) < 0) { + git_error_clear(); + + if (!(error = git_config_add_file_ondisk( + parent, cfg_path, GIT_CONFIG_LEVEL_LOCAL, repo, false))) + error = git_config_open_level(out, parent, GIT_CONFIG_LEVEL_LOCAL); + } + + git_config_free(parent); + + return error; +} + +static int repo_init_fs_configs( + git_config *cfg, + const char *cfg_path, + const char *repo_dir, + const char *work_dir, + bool update_ignorecase, + bool use_env) +{ + int error = 0; + + if (!work_dir) + work_dir = repo_dir; + + if ((error = git_config_set_bool( + cfg, "core.filemode", is_chmod_supported(cfg_path))) < 0) + return error; + + if (!are_symlinks_supported(work_dir, use_env)) { + if ((error = git_config_set_bool(cfg, "core.symlinks", false)) < 0) + return error; + } else if (git_config_delete_entry(cfg, "core.symlinks") < 0) + git_error_clear(); + + if (update_ignorecase) { + if (is_filesystem_case_insensitive(repo_dir)) { + if ((error = git_config_set_bool(cfg, "core.ignorecase", true)) < 0) + return error; + } else if (git_config_delete_entry(cfg, "core.ignorecase") < 0) + git_error_clear(); + } + +#ifdef GIT_USE_ICONV + if ((error = git_config_set_bool( + cfg, "core.precomposeunicode", + git_fs_path_does_decompose_unicode(work_dir))) < 0) + return error; + /* on non-iconv platforms, don't even set core.precomposeunicode */ +#endif + + return 0; +} + +static int repo_init_config( + const char *repo_dir, + const char *work_dir, + uint32_t flags, + uint32_t mode, + git_oid_t oid_type) +{ + int error = 0; + git_str cfg_path = GIT_STR_INIT, worktree_path = GIT_STR_INIT; + git_config *config = NULL; + bool is_bare = ((flags & GIT_REPOSITORY_INIT_BARE) != 0); + bool is_reinit = ((flags & GIT_REPOSITORY_INIT__IS_REINIT) != 0); + bool use_env = ((flags & GIT_REPOSITORY_OPEN_FROM_ENV) != 0); + int version = GIT_REPO_VERSION_DEFAULT; + + if ((error = repo_local_config(&config, &cfg_path, NULL, repo_dir)) < 0) + goto cleanup; + + if (is_reinit && + (error = check_repositoryformatversion(&version, config)) < 0) + goto cleanup; + + if ((error = check_extensions(config, version)) < 0) + goto cleanup; + +#define SET_REPO_CONFIG(TYPE, NAME, VAL) do { \ + if ((error = git_config_set_##TYPE(config, NAME, VAL)) < 0) \ + goto cleanup; } while (0) + + SET_REPO_CONFIG(bool, "core.bare", is_bare); + SET_REPO_CONFIG(int32, "core.repositoryformatversion", version); + + if ((error = repo_init_fs_configs( + config, cfg_path.ptr, repo_dir, work_dir, + !is_reinit, use_env)) < 0) + goto cleanup; + + if (!is_bare) { + SET_REPO_CONFIG(bool, "core.logallrefupdates", true); + + if (!(flags & GIT_REPOSITORY_INIT__NATURAL_WD)) { + if ((error = git_str_sets(&worktree_path, work_dir)) < 0) + goto cleanup; + + if ((flags & GIT_REPOSITORY_INIT_RELATIVE_GITLINK)) + if ((error = git_fs_path_make_relative(&worktree_path, repo_dir)) < 0) + goto cleanup; + + SET_REPO_CONFIG(string, "core.worktree", worktree_path.ptr); + } else if (is_reinit) { + if (git_config_delete_entry(config, "core.worktree") < 0) + git_error_clear(); + } + } + + if (mode == GIT_REPOSITORY_INIT_SHARED_GROUP) { + SET_REPO_CONFIG(int32, "core.sharedrepository", 1); + SET_REPO_CONFIG(bool, "receive.denyNonFastforwards", true); + } + else if (mode == GIT_REPOSITORY_INIT_SHARED_ALL) { + SET_REPO_CONFIG(int32, "core.sharedrepository", 2); + SET_REPO_CONFIG(bool, "receive.denyNonFastforwards", true); + } + + if (oid_type != GIT_OID_DEFAULT) { + SET_REPO_CONFIG(int32, "core.repositoryformatversion", 1); + SET_REPO_CONFIG(string, "extensions.objectformat", git_oid_type_name(oid_type)); + } + +cleanup: + git_str_dispose(&cfg_path); + git_str_dispose(&worktree_path); + git_config_free(config); + + return error; +} + +static int repo_reinit_submodule_fs(git_submodule *sm, const char *n, void *p) +{ + git_repository *smrepo = NULL; + GIT_UNUSED(n); GIT_UNUSED(p); + + if (git_submodule_open(&smrepo, sm) < 0 || + git_repository_reinit_filesystem(smrepo, true) < 0) + git_error_clear(); + git_repository_free(smrepo); + + return 0; +} + +int git_repository_reinit_filesystem(git_repository *repo, int recurse) +{ + int error = 0; + git_str path = GIT_STR_INIT; + git_config *config = NULL; + const char *repo_dir = git_repository_path(repo); + + if (!(error = repo_local_config(&config, &path, repo, repo_dir))) + error = repo_init_fs_configs(config, path.ptr, repo_dir, + git_repository_workdir(repo), true, repo->use_env); + + git_config_free(config); + git_str_dispose(&path); + + git_repository__configmap_lookup_cache_clear(repo); + + if (!repo->is_bare && recurse) + (void)git_submodule_foreach(repo, repo_reinit_submodule_fs, NULL); + + return error; +} + +static int repo_write_template( + const char *git_dir, + bool allow_overwrite, + const char *file, + mode_t mode, + bool hidden, + const char *content) +{ + git_str path = GIT_STR_INIT; + int fd, error = 0, flags; + + if (git_str_joinpath(&path, git_dir, file) < 0) + return -1; + + if (allow_overwrite) + flags = O_WRONLY | O_CREAT | O_TRUNC; + else + flags = O_WRONLY | O_CREAT | O_EXCL; + + fd = p_open(git_str_cstr(&path), flags, mode); + + if (fd >= 0) { + error = p_write(fd, content, strlen(content)); + + p_close(fd); + } + else if (errno != EEXIST) + error = fd; + +#ifdef GIT_WIN32 + if (!error && hidden) { + if (git_win32__set_hidden(path.ptr, true) < 0) + error = -1; + } +#else + GIT_UNUSED(hidden); +#endif + + git_str_dispose(&path); + + if (error) + git_error_set(GIT_ERROR_OS, + "failed to initialize repository with template '%s'", file); + + return error; +} + +static int repo_write_gitlink( + const char *in_dir, const char *to_repo, bool use_relative_path) +{ + int error; + git_str buf = GIT_STR_INIT; + git_str path_to_repo = GIT_STR_INIT; + struct stat st; + + git_fs_path_dirname_r(&buf, to_repo); + git_fs_path_to_dir(&buf); + if (git_str_oom(&buf)) + return -1; + + /* don't write gitlink to natural workdir */ + if (git__suffixcmp(to_repo, "/" DOT_GIT "/") == 0 && + strcmp(in_dir, buf.ptr) == 0) + { + error = GIT_PASSTHROUGH; + goto cleanup; + } + + if ((error = git_str_joinpath(&buf, in_dir, DOT_GIT)) < 0) + goto cleanup; + + if (!p_stat(buf.ptr, &st) && !S_ISREG(st.st_mode)) { + git_error_set(GIT_ERROR_REPOSITORY, + "cannot overwrite gitlink file into path '%s'", in_dir); + error = GIT_EEXISTS; + goto cleanup; + } + + git_str_clear(&buf); + + error = git_str_sets(&path_to_repo, to_repo); + + if (!error && use_relative_path) + error = git_fs_path_make_relative(&path_to_repo, in_dir); + + if (!error) + error = git_str_join(&buf, ' ', GIT_FILE_CONTENT_PREFIX, path_to_repo.ptr); + + if (!error) + error = repo_write_template(in_dir, true, DOT_GIT, 0666, true, buf.ptr); + +cleanup: + git_str_dispose(&buf); + git_str_dispose(&path_to_repo); + return error; +} + +static mode_t pick_dir_mode(git_repository_init_options *opts) +{ + if (opts->mode == GIT_REPOSITORY_INIT_SHARED_UMASK) + return 0777; + if (opts->mode == GIT_REPOSITORY_INIT_SHARED_GROUP) + return (0775 | S_ISGID); + if (opts->mode == GIT_REPOSITORY_INIT_SHARED_ALL) + return (0777 | S_ISGID); + return opts->mode; +} + +#include "repo_template.h" + +static int repo_init_structure( + const char *repo_dir, + const char *work_dir, + git_repository_init_options *opts) +{ + int error = 0; + repo_template_item *tpl; + bool external_tpl = + ((opts->flags & GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE) != 0); + mode_t dmode = pick_dir_mode(opts); + bool chmod = opts->mode != GIT_REPOSITORY_INIT_SHARED_UMASK; + + /* Hide the ".git" directory */ +#ifdef GIT_WIN32 + if ((opts->flags & GIT_REPOSITORY_INIT__HAS_DOTGIT) != 0) { + if (git_win32__set_hidden(repo_dir, true) < 0) { + git_error_set(GIT_ERROR_OS, + "failed to mark Git repository folder as hidden"); + return -1; + } + } +#endif + + /* Create the .git gitlink if appropriate */ + if ((opts->flags & GIT_REPOSITORY_INIT_BARE) == 0 && + (opts->flags & GIT_REPOSITORY_INIT__NATURAL_WD) == 0) + { + if (repo_write_gitlink(work_dir, repo_dir, opts->flags & GIT_REPOSITORY_INIT_RELATIVE_GITLINK) < 0) + return -1; + } + + /* Copy external template if requested */ + if (external_tpl) { + git_config *cfg = NULL; + const char *tdir = NULL; + bool default_template = false; + git_str template_buf = GIT_STR_INIT; + + if (opts->template_path) + tdir = opts->template_path; + else if ((error = git_config_open_default(&cfg)) >= 0) { + if (!git_config__get_path(&template_buf, cfg, "init.templatedir")) + tdir = template_buf.ptr; + git_error_clear(); + } + + if (!tdir) { + if (!(error = git_sysdir_find_template_dir(&template_buf))) + tdir = template_buf.ptr; + default_template = true; + } + + /* + * If tdir was the empty string, treat it like tdir was a path to an + * empty directory (so, don't do any copying). This is the behavior + * that git(1) exhibits, although it doesn't seem to be officially + * documented. + */ + if (tdir && git__strcmp(tdir, "") != 0) { + uint32_t cpflags = GIT_CPDIR_COPY_SYMLINKS | + GIT_CPDIR_SIMPLE_TO_MODE | + GIT_CPDIR_COPY_DOTFILES; + if (opts->mode != GIT_REPOSITORY_INIT_SHARED_UMASK) + cpflags |= GIT_CPDIR_CHMOD_DIRS; + error = git_futils_cp_r(tdir, repo_dir, cpflags, dmode); + } + + git_str_dispose(&template_buf); + git_config_free(cfg); + + /* If tdir does not exist, then do not error out. This matches the + * behaviour of git(1), which just prints a warning and continues. + * TODO: issue warning when warning API is available. + * `git` prints to stderr: 'warning: templates not found in /path/to/tdir' + */ + if (error < 0) { + if (!default_template && error != GIT_ENOTFOUND) + return error; + + /* if template was default, ignore error and use internal */ + git_error_clear(); + external_tpl = false; + error = 0; + } + } + + /* Copy internal template + * - always ensure existence of dirs + * - only create files if no external template was specified + */ + for (tpl = repo_template; !error && tpl->path; ++tpl) { + if (!tpl->content) { + uint32_t mkdir_flags = GIT_MKDIR_PATH; + if (chmod) + mkdir_flags |= GIT_MKDIR_CHMOD; + + error = git_futils_mkdir_relative( + tpl->path, repo_dir, dmode, mkdir_flags, NULL); + } + else if (!external_tpl) { + const char *content = tpl->content; + + if (opts->description && strcmp(tpl->path, GIT_DESC_FILE) == 0) + content = opts->description; + + error = repo_write_template( + repo_dir, false, tpl->path, tpl->mode, false, content); + } + } + + return error; +} + +static int mkdir_parent(git_str *buf, uint32_t mode, bool skip2) +{ + /* When making parent directories during repository initialization + * don't try to set gid or grant world write access + */ + return git_futils_mkdir( + buf->ptr, mode & ~(S_ISGID | 0002), + GIT_MKDIR_PATH | GIT_MKDIR_VERIFY_DIR | + (skip2 ? GIT_MKDIR_SKIP_LAST2 : GIT_MKDIR_SKIP_LAST)); +} + +static int repo_init_directories( + git_str *repo_path, + git_str *wd_path, + const char *given_repo, + git_repository_init_options *opts) +{ + int error = 0; + bool is_bare, add_dotgit, has_dotgit, natural_wd; + mode_t dirmode; + + /* There are three possible rules for what we are allowed to create: + * - MKPATH means anything we need + * - MKDIR means just the .git directory and its parent and the workdir + * - Neither means only the .git directory can be created + * + * There are 5 "segments" of path that we might need to deal with: + * 1. The .git directory + * 2. The parent of the .git directory + * 3. Everything above the parent of the .git directory + * 4. The working directory (often the same as #2) + * 5. Everything above the working directory (often the same as #3) + * + * For all directories created, we start with the init_mode value for + * permissions and then strip off bits in some cases: + * + * For MKPATH, we create #3 (and #5) paths without S_ISGID or S_IWOTH + * For MKPATH and MKDIR, we create #2 (and #4) without S_ISGID + * For all rules, we create #1 using the untouched init_mode + */ + + /* set up repo path */ + + is_bare = ((opts->flags & GIT_REPOSITORY_INIT_BARE) != 0); + + add_dotgit = + (opts->flags & GIT_REPOSITORY_INIT_NO_DOTGIT_DIR) == 0 && + !is_bare && + git__suffixcmp(given_repo, "/" DOT_GIT) != 0 && + git__suffixcmp(given_repo, "/" GIT_DIR) != 0; + + if (git_str_joinpath(repo_path, given_repo, add_dotgit ? GIT_DIR : "") < 0) + return -1; + + has_dotgit = (git__suffixcmp(repo_path->ptr, "/" GIT_DIR) == 0); + if (has_dotgit) + opts->flags |= GIT_REPOSITORY_INIT__HAS_DOTGIT; + + /* set up workdir path */ + + if (!is_bare) { + if (opts->workdir_path) { + if (git_fs_path_join_unrooted( + wd_path, opts->workdir_path, repo_path->ptr, NULL) < 0) + return -1; + } else if (has_dotgit) { + if (git_fs_path_dirname_r(wd_path, repo_path->ptr) < 0) + return -1; + } else { + git_error_set(GIT_ERROR_REPOSITORY, "cannot pick working directory" + " for non-bare repository that isn't a '.git' directory"); + return -1; + } + + if (git_fs_path_to_dir(wd_path) < 0) + return -1; + } else { + git_str_clear(wd_path); + } + + natural_wd = + has_dotgit && + wd_path->size > 0 && + wd_path->size + strlen(GIT_DIR) == repo_path->size && + memcmp(repo_path->ptr, wd_path->ptr, wd_path->size) == 0; + if (natural_wd) + opts->flags |= GIT_REPOSITORY_INIT__NATURAL_WD; + + /* create directories as needed / requested */ + + dirmode = pick_dir_mode(opts); + + if ((opts->flags & GIT_REPOSITORY_INIT_MKPATH) != 0) { + /* create path #5 */ + if (wd_path->size > 0 && + (error = mkdir_parent(wd_path, dirmode, false)) < 0) + return error; + + /* create path #3 (if not the same as #5) */ + if (!natural_wd && + (error = mkdir_parent(repo_path, dirmode, has_dotgit)) < 0) + return error; + } + + if ((opts->flags & GIT_REPOSITORY_INIT_MKDIR) != 0 || + (opts->flags & GIT_REPOSITORY_INIT_MKPATH) != 0) + { + /* create path #4 */ + if (wd_path->size > 0 && + (error = git_futils_mkdir( + wd_path->ptr, dirmode & ~S_ISGID, + GIT_MKDIR_VERIFY_DIR)) < 0) + return error; + + /* create path #2 (if not the same as #4) */ + if (!natural_wd && + (error = git_futils_mkdir( + repo_path->ptr, dirmode & ~S_ISGID, + GIT_MKDIR_VERIFY_DIR | GIT_MKDIR_SKIP_LAST)) < 0) + return error; + } + + if ((opts->flags & GIT_REPOSITORY_INIT_MKDIR) != 0 || + (opts->flags & GIT_REPOSITORY_INIT_MKPATH) != 0 || + has_dotgit) + { + /* create path #1 */ + error = git_futils_mkdir(repo_path->ptr, dirmode, + GIT_MKDIR_VERIFY_DIR | ((dirmode & S_ISGID) ? GIT_MKDIR_CHMOD : 0)); + } + + /* prettify both directories now that they are created */ + + if (!error) { + error = git_fs_path_prettify_dir(repo_path, repo_path->ptr, NULL); + + if (!error && wd_path->size > 0) + error = git_fs_path_prettify_dir(wd_path, wd_path->ptr, NULL); + } + + return error; +} + +static int repo_init_head(const char *repo_dir, const char *given) +{ + git_config *cfg = NULL; + git_str head_path = GIT_STR_INIT, cfg_branch = GIT_STR_INIT; + const char *initial_head = NULL; + int error; + + if ((error = git_str_joinpath(&head_path, repo_dir, GIT_HEAD_FILE)) < 0) + goto out; + + /* + * A template may have set a HEAD; use that unless it's been + * overridden by the caller's given initial head setting. + */ + if (git_fs_path_exists(head_path.ptr) && !given) + goto out; + + if (given) { + initial_head = given; + } else if ((error = git_config_open_default(&cfg)) >= 0 && + (error = git_config__get_string_buf(&cfg_branch, cfg, "init.defaultbranch")) >= 0 && + *cfg_branch.ptr) { + initial_head = cfg_branch.ptr; + } + + if (!initial_head) + initial_head = GIT_BRANCH_DEFAULT; + + error = git_repository_create_head(repo_dir, initial_head); + +out: + git_config_free(cfg); + git_str_dispose(&head_path); + git_str_dispose(&cfg_branch); + + return error; +} + +static int repo_init_create_origin(git_repository *repo, const char *url) +{ + int error; + git_remote *remote; + + if (!(error = git_remote_create(&remote, repo, GIT_REMOTE_ORIGIN, url))) { + git_remote_free(remote); + } + + return error; +} + +int git_repository_init( + git_repository **repo_out, const char *path, unsigned is_bare) +{ + git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT; + + opts.flags = GIT_REPOSITORY_INIT_MKPATH; /* don't love this default */ + if (is_bare) + opts.flags |= GIT_REPOSITORY_INIT_BARE; + + return git_repository_init_ext(repo_out, path, &opts); +} + +int git_repository_init_ext( + git_repository **out, + const char *given_repo, + git_repository_init_options *opts) +{ + git_str repo_path = GIT_STR_INIT, wd_path = GIT_STR_INIT, + common_path = GIT_STR_INIT; + const char *wd; + bool is_valid; + git_oid_t oid_type = GIT_OID_DEFAULT; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(given_repo); + GIT_ASSERT_ARG(opts); + + GIT_ERROR_CHECK_VERSION(opts, GIT_REPOSITORY_INIT_OPTIONS_VERSION, "git_repository_init_options"); + +#ifdef GIT_EXPERIMENTAL_SHA256 + if (opts->oid_type) + oid_type = opts->oid_type; +#endif + + if ((error = repo_init_directories(&repo_path, &wd_path, given_repo, opts)) < 0) + goto out; + + wd = (opts->flags & GIT_REPOSITORY_INIT_BARE) ? NULL : git_str_cstr(&wd_path); + + if ((error = is_valid_repository_path(&is_valid, &repo_path, &common_path, opts->flags)) < 0) + goto out; + + if (is_valid) { + if ((opts->flags & GIT_REPOSITORY_INIT_NO_REINIT) != 0) { + git_error_set(GIT_ERROR_REPOSITORY, + "attempt to reinitialize '%s'", given_repo); + error = GIT_EEXISTS; + goto out; + } + + opts->flags |= GIT_REPOSITORY_INIT__IS_REINIT; + + if ((error = repo_init_config(repo_path.ptr, wd, opts->flags, opts->mode, oid_type)) < 0) + goto out; + + /* TODO: reinitialize the templates */ + } else { + if ((error = repo_init_structure(repo_path.ptr, wd, opts)) < 0 || + (error = repo_init_config(repo_path.ptr, wd, opts->flags, opts->mode, oid_type)) < 0 || + (error = repo_init_head(repo_path.ptr, opts->initial_head)) < 0) + goto out; + } + + if ((error = git_repository_open(out, repo_path.ptr)) < 0) + goto out; + + if (opts->origin_url && + (error = repo_init_create_origin(*out, opts->origin_url)) < 0) + goto out; + +out: + git_str_dispose(&common_path); + git_str_dispose(&repo_path); + git_str_dispose(&wd_path); + + return error; +} + +int git_repository_head_detached(git_repository *repo) +{ + git_reference *ref; + git_odb *odb = NULL; + int exists; + + if (git_repository_odb__weakptr(&odb, repo) < 0) + return -1; + + if (git_reference_lookup(&ref, repo, GIT_HEAD_FILE) < 0) + return -1; + + if (git_reference_type(ref) == GIT_REFERENCE_SYMBOLIC) { + git_reference_free(ref); + return 0; + } + + exists = git_odb_exists(odb, git_reference_target(ref)); + + git_reference_free(ref); + return exists; +} + +int git_repository_head_detached_for_worktree(git_repository *repo, const char *name) +{ + git_reference *ref = NULL; + int error; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + if ((error = git_repository_head_for_worktree(&ref, repo, name)) < 0) + goto out; + + error = (git_reference_type(ref) != GIT_REFERENCE_SYMBOLIC); +out: + git_reference_free(ref); + + return error; +} + +int git_repository_head(git_reference **head_out, git_repository *repo) +{ + git_reference *head; + int error; + + GIT_ASSERT_ARG(head_out); + + if ((error = git_reference_lookup(&head, repo, GIT_HEAD_FILE)) < 0) + return error; + + if (git_reference_type(head) == GIT_REFERENCE_DIRECT) { + *head_out = head; + return 0; + } + + error = git_reference_lookup_resolved(head_out, repo, git_reference_symbolic_target(head), -1); + git_reference_free(head); + + return error == GIT_ENOTFOUND ? GIT_EUNBORNBRANCH : error; +} + +int git_repository_head_for_worktree(git_reference **out, git_repository *repo, const char *name) +{ + git_repository *worktree_repo = NULL; + git_worktree *worktree = NULL; + git_reference *head = NULL; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + *out = NULL; + + if ((error = git_worktree_lookup(&worktree, repo, name)) < 0 || + (error = git_repository_open_from_worktree(&worktree_repo, worktree)) < 0 || + (error = git_reference_lookup(&head, worktree_repo, GIT_HEAD_FILE)) < 0) + goto out; + + if (git_reference_type(head) != GIT_REFERENCE_DIRECT) { + if ((error = git_reference_lookup_resolved(out, worktree_repo, git_reference_symbolic_target(head), -1)) < 0) + goto out; + } else { + *out = head; + head = NULL; + } + +out: + git_reference_free(head); + git_worktree_free(worktree); + git_repository_free(worktree_repo); + return error; +} + +int git_repository_foreach_worktree(git_repository *repo, + git_repository_foreach_worktree_cb cb, + void *payload) +{ + git_strarray worktrees = {0}; + git_repository *worktree_repo = NULL; + git_worktree *worktree = NULL; + int error; + size_t i; + + /* apply operation to repository supplied when commondir is empty, implying there's + * no linked worktrees to iterate, which can occur when using custom odb/refdb + */ + if (!repo->commondir) + return cb(repo, payload); + + if ((error = git_repository_open(&worktree_repo, repo->commondir)) < 0 || + (error = cb(worktree_repo, payload) != 0)) + goto out; + + git_repository_free(worktree_repo); + worktree_repo = NULL; + + if ((error = git_worktree_list(&worktrees, repo)) < 0) + goto out; + + for (i = 0; i < worktrees.count; i++) { + git_repository_free(worktree_repo); + worktree_repo = NULL; + git_worktree_free(worktree); + worktree = NULL; + + if ((error = git_worktree_lookup(&worktree, repo, worktrees.strings[i]) < 0) || + (error = git_repository_open_from_worktree(&worktree_repo, worktree)) < 0) { + if (error != GIT_ENOTFOUND) + goto out; + error = 0; + continue; + } + + if ((error = cb(worktree_repo, payload)) != 0) + goto out; + } + +out: + git_strarray_dispose(&worktrees); + git_repository_free(worktree_repo); + git_worktree_free(worktree); + return error; +} + +int git_repository_head_unborn(git_repository *repo) +{ + git_reference *ref = NULL; + int error; + + error = git_repository_head(&ref, repo); + git_reference_free(ref); + + if (error == GIT_EUNBORNBRANCH) { + git_error_clear(); + return 1; + } + + if (error < 0) + return -1; + + return 0; +} + +static int repo_contains_no_reference(git_repository *repo) +{ + git_reference_iterator *iter; + const char *refname; + int error; + + if ((error = git_reference_iterator_new(&iter, repo)) < 0) + return error; + + error = git_reference_next_name(&refname, iter); + git_reference_iterator_free(iter); + + if (error == GIT_ITEROVER) + return 1; + + return error; +} + +int git_repository_initialbranch(git_str *out, git_repository *repo) +{ + git_config *config; + git_config_entry *entry = NULL; + const char *branch; + int valid, error; + + if ((error = git_repository_config__weakptr(&config, repo)) < 0) + return error; + + if ((error = git_config_get_entry(&entry, config, "init.defaultbranch")) == 0 && + *entry->value) { + branch = entry->value; + } + else if (!error || error == GIT_ENOTFOUND) { + branch = GIT_BRANCH_DEFAULT; + } + else { + goto done; + } + + if ((error = git_str_puts(out, GIT_REFS_HEADS_DIR)) < 0 || + (error = git_str_puts(out, branch)) < 0 || + (error = git_reference_name_is_valid(&valid, out->ptr)) < 0) + goto done; + + if (!valid) { + git_error_set(GIT_ERROR_INVALID, "the value of init.defaultBranch is not a valid branch name"); + error = -1; + } + +done: + git_config_entry_free(entry); + return error; +} + +int git_repository_is_empty(git_repository *repo) +{ + git_reference *head = NULL; + git_str initialbranch = GIT_STR_INIT; + int result = 0; + + if ((result = git_reference_lookup(&head, repo, GIT_HEAD_FILE)) < 0 || + (result = git_repository_initialbranch(&initialbranch, repo)) < 0) + goto done; + + result = (git_reference_type(head) == GIT_REFERENCE_SYMBOLIC && + strcmp(git_reference_symbolic_target(head), initialbranch.ptr) == 0 && + repo_contains_no_reference(repo)); + +done: + git_reference_free(head); + git_str_dispose(&initialbranch); + + return result; +} + +static const char *resolved_parent_path(const git_repository *repo, git_repository_item_t item, git_repository_item_t fallback) +{ + const char *parent; + + switch (item) { + case GIT_REPOSITORY_ITEM_GITDIR: + parent = git_repository_path(repo); + break; + case GIT_REPOSITORY_ITEM_WORKDIR: + parent = git_repository_workdir(repo); + break; + case GIT_REPOSITORY_ITEM_COMMONDIR: + parent = git_repository_commondir(repo); + break; + default: + git_error_set(GIT_ERROR_INVALID, "invalid item directory"); + return NULL; + } + if (!parent && fallback != GIT_REPOSITORY_ITEM__LAST) + return resolved_parent_path(repo, fallback, GIT_REPOSITORY_ITEM__LAST); + + return parent; +} + +int git_repository_item_path( + git_buf *out, + const git_repository *repo, + git_repository_item_t item) +{ + GIT_BUF_WRAP_PRIVATE(out, git_repository__item_path, repo, item); +} + +int git_repository__item_path( + git_str *out, + const git_repository *repo, + git_repository_item_t item) +{ + const char *parent = resolved_parent_path(repo, items[item].parent, items[item].fallback); + if (parent == NULL) { + git_error_set(GIT_ERROR_INVALID, "path cannot exist in repository"); + return GIT_ENOTFOUND; + } + + if (git_str_sets(out, parent) < 0) + return -1; + + if (items[item].name) { + if (git_str_joinpath(out, parent, items[item].name) < 0) + return -1; + } + + if (items[item].directory) { + if (git_fs_path_to_dir(out) < 0) + return -1; + } + + return 0; +} + +const char *git_repository_path(const git_repository *repo) +{ + GIT_ASSERT_ARG_WITH_RETVAL(repo, NULL); + return repo->gitdir; +} + +const char *git_repository_workdir(const git_repository *repo) +{ + GIT_ASSERT_ARG_WITH_RETVAL(repo, NULL); + + if (repo->is_bare) + return NULL; + + return repo->workdir; +} + +int git_repository_workdir_path( + git_str *out, git_repository *repo, const char *path) +{ + int error; + + if (!repo->workdir) { + git_error_set(GIT_ERROR_REPOSITORY, "repository has no working directory"); + return GIT_EBAREREPO; + } + + if (!(error = git_str_joinpath(out, repo->workdir, path))) + error = git_path_validate_str_length(repo, out); + + return error; +} + +const char *git_repository_commondir(const git_repository *repo) +{ + GIT_ASSERT_ARG_WITH_RETVAL(repo, NULL); + return repo->commondir; +} + +int git_repository_set_workdir( + git_repository *repo, const char *workdir, int update_gitlink) +{ + int error = 0; + git_str path = GIT_STR_INIT; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(workdir); + + if (git_fs_path_prettify_dir(&path, workdir, NULL) < 0) + return -1; + + if (repo->workdir && strcmp(repo->workdir, path.ptr) == 0) + return 0; + + if (update_gitlink) { + git_config *config; + + if (git_repository_config__weakptr(&config, repo) < 0) + return -1; + + error = repo_write_gitlink(path.ptr, git_repository_path(repo), false); + + /* passthrough error means gitlink is unnecessary */ + if (error == GIT_PASSTHROUGH) + error = git_config_delete_entry(config, "core.worktree"); + else if (!error) + error = git_config_set_string(config, "core.worktree", path.ptr); + + if (!error) + error = git_config_set_bool(config, "core.bare", false); + } + + if (!error) { + char *old_workdir = repo->workdir; + + repo->workdir = git_str_detach(&path); + repo->is_bare = 0; + + git__free(old_workdir); + } + + return error; +} + +int git_repository_is_bare(const git_repository *repo) +{ + GIT_ASSERT_ARG(repo); + return repo->is_bare; +} + +int git_repository_is_worktree(const git_repository *repo) +{ + GIT_ASSERT_ARG(repo); + return repo->is_worktree; +} + +int git_repository_set_bare(git_repository *repo) +{ + int error; + git_config *config; + + GIT_ASSERT_ARG(repo); + + if (repo->is_bare) + return 0; + + if ((error = git_repository_config__weakptr(&config, repo)) < 0) + return error; + + if ((error = git_config_set_bool(config, "core.bare", true)) < 0) + return error; + + if ((error = git_config__update_entry(config, "core.worktree", NULL, true, true)) < 0) + return error; + + git__free(repo->workdir); + repo->workdir = NULL; + repo->is_bare = 1; + + return 0; +} + +int git_repository_head_tree(git_tree **tree, git_repository *repo) +{ + git_reference *head; + git_object *obj; + int error; + + if ((error = git_repository_head(&head, repo)) < 0) + return error; + + if ((error = git_reference_peel(&obj, head, GIT_OBJECT_TREE)) < 0) + goto cleanup; + + *tree = (git_tree *)obj; + +cleanup: + git_reference_free(head); + return error; +} + +int git_repository__set_orig_head(git_repository *repo, const git_oid *orig_head) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_str file_path = GIT_STR_INIT; + char orig_head_str[GIT_OID_MAX_HEXSIZE]; + int error = 0; + + git_oid_fmt(orig_head_str, orig_head); + + if ((error = git_str_joinpath(&file_path, repo->gitdir, GIT_ORIG_HEAD_FILE)) == 0 && + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_CREATE_LEADING_DIRS, GIT_MERGE_FILE_MODE)) == 0 && + (error = git_filebuf_printf(&file, "%.*s\n", (int)git_oid_hexsize(repo->oid_type), orig_head_str)) == 0) + error = git_filebuf_commit(&file); + + if (error < 0) + git_filebuf_cleanup(&file); + + git_str_dispose(&file_path); + + return error; +} + +static int git_repository__message(git_str *out, git_repository *repo) +{ + git_str path = GIT_STR_INIT; + struct stat st; + int error; + + if (git_str_joinpath(&path, repo->gitdir, GIT_MERGE_MSG_FILE) < 0) + return -1; + + if ((error = p_stat(git_str_cstr(&path), &st)) < 0) { + if (errno == ENOENT) + error = GIT_ENOTFOUND; + git_error_set(GIT_ERROR_OS, "could not access message file"); + } else { + error = git_futils_readbuffer(out, git_str_cstr(&path)); + } + + git_str_dispose(&path); + + return error; +} + +int git_repository_message(git_buf *out, git_repository *repo) +{ + GIT_BUF_WRAP_PRIVATE(out, git_repository__message, repo); +} + +int git_repository_message_remove(git_repository *repo) +{ + git_str path = GIT_STR_INIT; + int error; + + if (git_str_joinpath(&path, repo->gitdir, GIT_MERGE_MSG_FILE) < 0) + return -1; + + error = p_unlink(git_str_cstr(&path)); + git_str_dispose(&path); + + return error; +} + +int git_repository_hashfile( + git_oid *out, + git_repository *repo, + const char *path, + git_object_t type, + const char *as_path) +{ + int error; + git_filter_list *fl = NULL; + git_file fd = -1; + uint64_t len; + git_str full_path = GIT_STR_INIT; + const char *workdir = git_repository_workdir(repo); + + /* as_path can be NULL */ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(path); + GIT_ASSERT_ARG(repo); + + if ((error = git_fs_path_join_unrooted(&full_path, path, workdir, NULL)) < 0 || + (error = git_path_validate_str_length(repo, &full_path)) < 0) + return error; + + /* + * NULL as_path means that we should derive it from the + * given path. + */ + if (!as_path) { + if (workdir && !git__prefixcmp(full_path.ptr, workdir)) + as_path = full_path.ptr + strlen(workdir); + else + as_path = ""; + } + + /* passing empty string for "as_path" indicated --no-filters */ + if (strlen(as_path) > 0) { + error = git_filter_list_load( + &fl, repo, NULL, as_path, + GIT_FILTER_TO_ODB, GIT_FILTER_DEFAULT); + + if (error < 0) + return error; + } + + /* at this point, error is a count of the number of loaded filters */ + + fd = git_futils_open_ro(full_path.ptr); + if (fd < 0) { + error = fd; + goto cleanup; + } + + if ((error = git_futils_filesize(&len, fd)) < 0) + goto cleanup; + + if (!git__is_sizet(len)) { + git_error_set(GIT_ERROR_OS, "file size overflow for 32-bit systems"); + error = -1; + goto cleanup; + } + + error = git_odb__hashfd_filtered(out, fd, (size_t)len, type, repo->oid_type, fl); + +cleanup: + if (fd >= 0) + p_close(fd); + git_filter_list_free(fl); + git_str_dispose(&full_path); + + return error; +} + +static int checkout_message(git_str *out, git_reference *old, const char *new) +{ + const char *idstr; + + git_str_puts(out, "checkout: moving from "); + + if (git_reference_type(old) == GIT_REFERENCE_SYMBOLIC) { + git_str_puts(out, git_reference__shorthand(git_reference_symbolic_target(old))); + } else { + if ((idstr = git_oid_tostr_s(git_reference_target(old))) == NULL) + return -1; + + git_str_puts(out, idstr); + } + + git_str_puts(out, " to "); + + if (git_reference__is_branch(new) || + git_reference__is_tag(new) || + git_reference__is_remote(new)) + git_str_puts(out, git_reference__shorthand(new)); + else + git_str_puts(out, new); + + if (git_str_oom(out)) + return -1; + + return 0; +} + +static int detach(git_repository *repo, const git_oid *id, const char *new) +{ + int error; + git_str log_message = GIT_STR_INIT; + git_object *object = NULL, *peeled = NULL; + git_reference *new_head = NULL, *current = NULL; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(id); + + if ((error = git_reference_lookup(¤t, repo, GIT_HEAD_FILE)) < 0) + return error; + + if ((error = git_object_lookup(&object, repo, id, GIT_OBJECT_ANY)) < 0) + goto cleanup; + + if ((error = git_object_peel(&peeled, object, GIT_OBJECT_COMMIT)) < 0) + goto cleanup; + + if (new == NULL && + (new = git_oid_tostr_s(git_object_id(peeled))) == NULL) { + error = -1; + goto cleanup; + } + + if ((error = checkout_message(&log_message, current, new)) < 0) + goto cleanup; + + error = git_reference_create(&new_head, repo, GIT_HEAD_FILE, git_object_id(peeled), true, git_str_cstr(&log_message)); + +cleanup: + git_str_dispose(&log_message); + git_object_free(object); + git_object_free(peeled); + git_reference_free(current); + git_reference_free(new_head); + return error; +} + +int git_repository_set_head( + git_repository *repo, + const char *refname) +{ + git_reference *ref = NULL, *current = NULL, *new_head = NULL; + git_str log_message = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(refname); + + if ((error = git_reference_lookup(¤t, repo, GIT_HEAD_FILE)) < 0) + return error; + + if ((error = checkout_message(&log_message, current, refname)) < 0) + goto cleanup; + + error = git_reference_lookup(&ref, repo, refname); + if (error < 0 && error != GIT_ENOTFOUND) + goto cleanup; + + if (ref && current->type == GIT_REFERENCE_SYMBOLIC && git__strcmp(current->target.symbolic, ref->name) && + git_reference_is_branch(ref) && git_branch_is_checked_out(ref)) { + git_error_set(GIT_ERROR_REPOSITORY, "cannot set HEAD to reference '%s' as it is the current HEAD " + "of a linked repository.", git_reference_name(ref)); + error = -1; + goto cleanup; + } + + if (!error) { + if (git_reference_is_branch(ref)) { + error = git_reference_symbolic_create(&new_head, repo, GIT_HEAD_FILE, + git_reference_name(ref), true, git_str_cstr(&log_message)); + } else { + error = detach(repo, git_reference_target(ref), + git_reference_is_tag(ref) || git_reference_is_remote(ref) ? refname : NULL); + } + } else if (git_reference__is_branch(refname)) { + error = git_reference_symbolic_create(&new_head, repo, GIT_HEAD_FILE, refname, + true, git_str_cstr(&log_message)); + } + +cleanup: + git_str_dispose(&log_message); + git_reference_free(current); + git_reference_free(ref); + git_reference_free(new_head); + return error; +} + +int git_repository_set_head_detached( + git_repository *repo, + const git_oid *committish) +{ + return detach(repo, committish, NULL); +} + +int git_repository_set_head_detached_from_annotated( + git_repository *repo, + const git_annotated_commit *committish) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(committish); + + return detach(repo, git_annotated_commit_id(committish), committish->description); +} + +int git_repository_detach_head(git_repository *repo) +{ + git_reference *old_head = NULL, *new_head = NULL, *current = NULL; + git_object *object = NULL; + git_str log_message = GIT_STR_INIT; + const char *idstr; + int error; + + GIT_ASSERT_ARG(repo); + + if ((error = git_reference_lookup(¤t, repo, GIT_HEAD_FILE)) < 0) + return error; + + if ((error = git_repository_head(&old_head, repo)) < 0) + goto cleanup; + + if ((error = git_object_lookup(&object, repo, git_reference_target(old_head), GIT_OBJECT_COMMIT)) < 0) + goto cleanup; + + if ((idstr = git_oid_tostr_s(git_object_id(object))) == NULL) { + error = -1; + goto cleanup; + } + + if ((error = checkout_message(&log_message, current, idstr)) < 0) + goto cleanup; + + error = git_reference_create(&new_head, repo, GIT_HEAD_FILE, git_reference_target(old_head), + 1, git_str_cstr(&log_message)); + +cleanup: + git_str_dispose(&log_message); + git_object_free(object); + git_reference_free(old_head); + git_reference_free(new_head); + git_reference_free(current); + return error; +} + +/** + * Loosely ported from git.git + * https://github.com/git/git/blob/master/contrib/completion/git-prompt.sh#L198-289 + */ +int git_repository_state(git_repository *repo) +{ + git_str repo_path = GIT_STR_INIT; + int state = GIT_REPOSITORY_STATE_NONE; + + GIT_ASSERT_ARG(repo); + + if (git_str_puts(&repo_path, repo->gitdir) < 0) + return -1; + + if (git_fs_path_contains_file(&repo_path, GIT_REBASE_MERGE_INTERACTIVE_FILE)) + state = GIT_REPOSITORY_STATE_REBASE_INTERACTIVE; + else if (git_fs_path_contains_dir(&repo_path, GIT_REBASE_MERGE_DIR)) + state = GIT_REPOSITORY_STATE_REBASE_MERGE; + else if (git_fs_path_contains_file(&repo_path, GIT_REBASE_APPLY_REBASING_FILE)) + state = GIT_REPOSITORY_STATE_REBASE; + else if (git_fs_path_contains_file(&repo_path, GIT_REBASE_APPLY_APPLYING_FILE)) + state = GIT_REPOSITORY_STATE_APPLY_MAILBOX; + else if (git_fs_path_contains_dir(&repo_path, GIT_REBASE_APPLY_DIR)) + state = GIT_REPOSITORY_STATE_APPLY_MAILBOX_OR_REBASE; + else if (git_fs_path_contains_file(&repo_path, GIT_MERGE_HEAD_FILE)) + state = GIT_REPOSITORY_STATE_MERGE; + else if (git_fs_path_contains_file(&repo_path, GIT_REVERT_HEAD_FILE)) { + state = GIT_REPOSITORY_STATE_REVERT; + if (git_fs_path_contains_file(&repo_path, GIT_SEQUENCER_TODO_FILE)) { + state = GIT_REPOSITORY_STATE_REVERT_SEQUENCE; + } + } else if (git_fs_path_contains_file(&repo_path, GIT_CHERRYPICK_HEAD_FILE)) { + state = GIT_REPOSITORY_STATE_CHERRYPICK; + if (git_fs_path_contains_file(&repo_path, GIT_SEQUENCER_TODO_FILE)) { + state = GIT_REPOSITORY_STATE_CHERRYPICK_SEQUENCE; + } + } else if (git_fs_path_contains_file(&repo_path, GIT_BISECT_LOG_FILE)) + state = GIT_REPOSITORY_STATE_BISECT; + + git_str_dispose(&repo_path); + return state; +} + +int git_repository__cleanup_files( + git_repository *repo, const char *files[], size_t files_len) +{ + git_str buf = GIT_STR_INIT; + size_t i; + int error; + + for (error = 0, i = 0; !error && i < files_len; ++i) { + const char *path; + + if (git_str_joinpath(&buf, repo->gitdir, files[i]) < 0) + return -1; + + path = git_str_cstr(&buf); + + if (git_fs_path_isfile(path)) { + error = p_unlink(path); + } else if (git_fs_path_isdir(path)) { + error = git_futils_rmdir_r(path, NULL, + GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_REMOVE_BLOCKERS); + } + + git_str_clear(&buf); + } + + git_str_dispose(&buf); + return error; +} + +static const char *state_files[] = { + GIT_MERGE_HEAD_FILE, + GIT_MERGE_MODE_FILE, + GIT_MERGE_MSG_FILE, + GIT_REVERT_HEAD_FILE, + GIT_CHERRYPICK_HEAD_FILE, + GIT_BISECT_LOG_FILE, + GIT_REBASE_MERGE_DIR, + GIT_REBASE_APPLY_DIR, + GIT_SEQUENCER_DIR, +}; + +int git_repository_state_cleanup(git_repository *repo) +{ + GIT_ASSERT_ARG(repo); + + return git_repository__cleanup_files(repo, state_files, ARRAY_SIZE(state_files)); +} + +int git_repository__shallow_roots( + git_oid **out, + size_t *out_len, + git_repository *repo) +{ + int error = 0; + + if (!repo->shallow_grafts && (error = load_grafts(repo)) < 0) + return error; + + if ((error = git_grafts_refresh(repo->shallow_grafts)) < 0) + return error; + + if ((error = git_grafts_oids(out, out_len, repo->shallow_grafts)) < 0) + return error; + + return 0; +} + +int git_repository__shallow_roots_write(git_repository *repo, git_oidarray *roots) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_str path = GIT_STR_INIT; + char oid_str[GIT_OID_MAX_HEXSIZE + 1]; + size_t i; + int filebuf_hash, error = 0; + + GIT_ASSERT_ARG(repo); + + filebuf_hash = git_filebuf_hash_flags(git_oid_algorithm(repo->oid_type)); + GIT_ASSERT(filebuf_hash); + + if ((error = git_str_joinpath(&path, repo->gitdir, "shallow")) < 0) + goto on_error; + + if ((error = git_filebuf_open(&file, git_str_cstr(&path), filebuf_hash, 0666)) < 0) + goto on_error; + + for (i = 0; i < roots->count; i++) { + git_oid_tostr(oid_str, sizeof(oid_str), &roots->ids[i]); + git_filebuf_write(&file, oid_str, git_oid_hexsize(repo->oid_type)); + git_filebuf_write(&file, "\n", 1); + } + + git_filebuf_commit(&file); + + if ((error = load_grafts(repo)) < 0) { + error = -1; + goto on_error; + } + + if (!roots->count) + remove(path.ptr); + +on_error: + git_str_dispose(&path); + + return error; +} + +int git_repository_is_shallow(git_repository *repo) +{ + git_str path = GIT_STR_INIT; + struct stat st; + int error; + + if ((error = git_str_joinpath(&path, repo->gitdir, "shallow")) < 0) + return error; + + error = git_fs_path_lstat(path.ptr, &st); + git_str_dispose(&path); + + if (error == GIT_ENOTFOUND) { + git_error_clear(); + return 0; + } + + if (error < 0) + return error; + + return st.st_size == 0 ? 0 : 1; +} + +int git_repository_init_options_init( + git_repository_init_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_repository_init_options, + GIT_REPOSITORY_INIT_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_repository_init_init_options( + git_repository_init_options *opts, unsigned int version) +{ + return git_repository_init_options_init(opts, version); +} +#endif + +int git_repository_ident(const char **name, const char **email, const git_repository *repo) +{ + *name = repo->ident_name; + *email = repo->ident_email; + + return 0; +} + +int git_repository_set_ident(git_repository *repo, const char *name, const char *email) +{ + char *tmp_name = NULL, *tmp_email = NULL; + + if (name) { + tmp_name = git__strdup(name); + GIT_ERROR_CHECK_ALLOC(tmp_name); + } + + if (email) { + tmp_email = git__strdup(email); + GIT_ERROR_CHECK_ALLOC(tmp_email); + } + + tmp_name = git_atomic_swap(repo->ident_name, tmp_name); + tmp_email = git_atomic_swap(repo->ident_email, tmp_email); + + git__free(tmp_name); + git__free(tmp_email); + + return 0; +} + +int git_repository_submodule_cache_all(git_repository *repo) +{ + GIT_ASSERT_ARG(repo); + return git_submodule_cache_init(&repo->submodule_cache, repo); +} + +int git_repository_submodule_cache_clear(git_repository *repo) +{ + int error = 0; + GIT_ASSERT_ARG(repo); + + error = git_submodule_cache_free(repo->submodule_cache); + repo->submodule_cache = NULL; + return error; +} + +git_oid_t git_repository_oid_type(git_repository *repo) +{ + return repo ? repo->oid_type : 0; +} diff --git a/src/libgit2/repository.h b/src/libgit2/repository.h new file mode 100644 index 0000000..6d2b64c --- /dev/null +++ b/src/libgit2/repository.h @@ -0,0 +1,283 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_repository_h__ +#define INCLUDE_repository_h__ + +#include "common.h" + +#include "git2/common.h" +#include "git2/oid.h" +#include "git2/odb.h" +#include "git2/repository.h" +#include "git2/object.h" +#include "git2/config.h" + +#include "array.h" +#include "cache.h" +#include "refs.h" +#include "str.h" +#include "object.h" +#include "attrcache.h" +#include "submodule.h" +#include "diff_driver.h" +#include "grafts.h" + +#define DOT_GIT ".git" +#define GIT_DIR DOT_GIT "/" +#define GIT_DIR_MODE 0755 +#define GIT_BARE_DIR_MODE 0777 + +/* Default DOS-compatible 8.3 "short name" for a git repository, "GIT~1" */ +#define GIT_DIR_SHORTNAME "GIT~1" + +extern bool git_repository__fsync_gitdir; +extern bool git_repository__validate_ownership; + +/** Cvar cache identifiers */ +typedef enum { + GIT_CONFIGMAP_AUTO_CRLF = 0, /* core.autocrlf */ + GIT_CONFIGMAP_EOL, /* core.eol */ + GIT_CONFIGMAP_SYMLINKS, /* core.symlinks */ + GIT_CONFIGMAP_IGNORECASE, /* core.ignorecase */ + GIT_CONFIGMAP_FILEMODE, /* core.filemode */ + GIT_CONFIGMAP_IGNORESTAT, /* core.ignorestat */ + GIT_CONFIGMAP_TRUSTCTIME, /* core.trustctime */ + GIT_CONFIGMAP_ABBREV, /* core.abbrev */ + GIT_CONFIGMAP_PRECOMPOSE, /* core.precomposeunicode */ + GIT_CONFIGMAP_SAFE_CRLF, /* core.safecrlf */ + GIT_CONFIGMAP_LOGALLREFUPDATES, /* core.logallrefupdates */ + GIT_CONFIGMAP_PROTECTHFS, /* core.protectHFS */ + GIT_CONFIGMAP_PROTECTNTFS, /* core.protectNTFS */ + GIT_CONFIGMAP_FSYNCOBJECTFILES, /* core.fsyncObjectFiles */ + GIT_CONFIGMAP_LONGPATHS, /* core.longpaths */ + GIT_CONFIGMAP_CACHE_MAX +} git_configmap_item; + +/** + * Configuration map value enumerations + * + * These are the values that are actually stored in the configmap cache, + * instead of their string equivalents. These values are internal and + * symbolic; make sure that none of them is set to `-1`, since that is + * the unique identifier for "not cached" + */ +typedef enum { + /* The value hasn't been loaded from the cache yet */ + GIT_CONFIGMAP_NOT_CACHED = -1, + + /* core.safecrlf: false, 'fail', 'warn' */ + GIT_SAFE_CRLF_FALSE = 0, + GIT_SAFE_CRLF_FAIL = 1, + GIT_SAFE_CRLF_WARN = 2, + + /* core.autocrlf: false, true, 'input; */ + GIT_AUTO_CRLF_FALSE = 0, + GIT_AUTO_CRLF_TRUE = 1, + GIT_AUTO_CRLF_INPUT = 2, + GIT_AUTO_CRLF_DEFAULT = GIT_AUTO_CRLF_FALSE, + + /* core.eol: unset, 'crlf', 'lf', 'native' */ + GIT_EOL_UNSET = 0, + GIT_EOL_CRLF = 1, + GIT_EOL_LF = 2, +#ifdef GIT_WIN32 + GIT_EOL_NATIVE = GIT_EOL_CRLF, +#else + GIT_EOL_NATIVE = GIT_EOL_LF, +#endif + GIT_EOL_DEFAULT = GIT_EOL_NATIVE, + + /* core.symlinks: bool */ + GIT_SYMLINKS_DEFAULT = GIT_CONFIGMAP_TRUE, + /* core.ignorecase */ + GIT_IGNORECASE_DEFAULT = GIT_CONFIGMAP_FALSE, + /* core.filemode */ + GIT_FILEMODE_DEFAULT = GIT_CONFIGMAP_TRUE, + /* core.ignorestat */ + GIT_IGNORESTAT_DEFAULT = GIT_CONFIGMAP_FALSE, + /* core.trustctime */ + GIT_TRUSTCTIME_DEFAULT = GIT_CONFIGMAP_TRUE, + /* core.abbrev */ + GIT_ABBREV_DEFAULT = 7, + /* core.precomposeunicode */ + GIT_PRECOMPOSE_DEFAULT = GIT_CONFIGMAP_FALSE, + /* core.safecrlf */ + GIT_SAFE_CRLF_DEFAULT = GIT_CONFIGMAP_FALSE, + /* core.logallrefupdates */ + GIT_LOGALLREFUPDATES_FALSE = GIT_CONFIGMAP_FALSE, + GIT_LOGALLREFUPDATES_TRUE = GIT_CONFIGMAP_TRUE, + GIT_LOGALLREFUPDATES_UNSET = 2, + GIT_LOGALLREFUPDATES_ALWAYS = 3, + GIT_LOGALLREFUPDATES_DEFAULT = GIT_LOGALLREFUPDATES_UNSET, + /* core.protectHFS */ + GIT_PROTECTHFS_DEFAULT = GIT_CONFIGMAP_FALSE, + /* core.protectNTFS */ + GIT_PROTECTNTFS_DEFAULT = GIT_CONFIGMAP_TRUE, + /* core.fsyncObjectFiles */ + GIT_FSYNCOBJECTFILES_DEFAULT = GIT_CONFIGMAP_FALSE, + /* core.longpaths */ + GIT_LONGPATHS_DEFAULT = GIT_CONFIGMAP_FALSE +} git_configmap_value; + +/* internal repository init flags */ +enum { + GIT_REPOSITORY_INIT__HAS_DOTGIT = (1u << 16), + GIT_REPOSITORY_INIT__NATURAL_WD = (1u << 17), + GIT_REPOSITORY_INIT__IS_REINIT = (1u << 18) +}; + +/** Internal structure for repository object */ +struct git_repository { + git_odb *_odb; + git_refdb *_refdb; + git_config *_config; + git_index *_index; + + git_cache objects; + git_attr_cache *attrcache; + git_diff_driver_registry *diff_drivers; + + char *gitlink; + char *gitdir; + char *commondir; + char *workdir; + char *namespace; + + char *ident_name; + char *ident_email; + + git_array_t(git_str) reserved_names; + + unsigned use_env:1, + is_bare:1, + is_worktree:1; + git_oid_t oid_type; + + unsigned int lru_counter; + + git_grafts *grafts; + git_grafts *shallow_grafts; + + git_atomic32 attr_session_key; + + intptr_t configmap_cache[GIT_CONFIGMAP_CACHE_MAX]; + git_strmap *submodule_cache; +}; + +GIT_INLINE(git_attr_cache *) git_repository_attr_cache(git_repository *repo) +{ + return repo->attrcache; +} + +int git_repository_head_tree(git_tree **tree, git_repository *repo); +int git_repository_create_head(const char *git_dir, const char *ref_name); + +typedef int (*git_repository_foreach_worktree_cb)(git_repository *, void *); + +int git_repository_foreach_worktree(git_repository *repo, + git_repository_foreach_worktree_cb cb, + void *payload); + +/* + * Weak pointers to repository internals. + * + * The returned pointers do not need to be freed. Do not keep + * permanent references to these (i.e. between API calls), since they may + * become invalidated if the user replaces a repository internal. + */ +int git_repository_config__weakptr(git_config **out, git_repository *repo); +int git_repository_odb__weakptr(git_odb **out, git_repository *repo); +int git_repository_refdb__weakptr(git_refdb **out, git_repository *repo); +int git_repository_index__weakptr(git_index **out, git_repository *repo); +int git_repository_grafts__weakptr(git_grafts **out, git_repository *repo); +int git_repository_shallow_grafts__weakptr(git_grafts **out, git_repository *repo); + +int git_repository__wrap_odb( + git_repository **out, + git_odb *odb, + git_oid_t oid_type); + +/* + * Configuration map cache + * + * Efficient access to the most used config variables of a repository. + * The cache is cleared every time the config backend is replaced. + */ +int git_repository__configmap_lookup(int *out, git_repository *repo, git_configmap_item item); +void git_repository__configmap_lookup_cache_clear(git_repository *repo); + +int git_repository__item_path(git_str *out, const git_repository *repo, git_repository_item_t item); + +GIT_INLINE(int) git_repository__ensure_not_bare( + git_repository *repo, + const char *operation_name) +{ + if (!git_repository_is_bare(repo)) + return 0; + + git_error_set( + GIT_ERROR_REPOSITORY, + "cannot %s. This operation is not allowed against bare repositories.", + operation_name); + + return GIT_EBAREREPO; +} + +int git_repository__set_orig_head(git_repository *repo, const git_oid *orig_head); + +int git_repository__cleanup_files(git_repository *repo, const char *files[], size_t files_len); + +/* The default "reserved names" for a repository */ +extern git_str git_repository__reserved_names_win32[]; +extern size_t git_repository__reserved_names_win32_len; + +extern git_str git_repository__reserved_names_posix[]; +extern size_t git_repository__reserved_names_posix_len; + +/* + * Gets any "reserved names" in the repository. This will return paths + * that should not be allowed in the repository (like ".git") to avoid + * conflicting with the repository path, or with alternate mechanisms to + * the repository path (eg, "GIT~1"). Every attempt will be made to look + * up all possible reserved names - if there was a conflict for the shortname + * GIT~1, for example, this function will try to look up the alternate + * shortname. If that fails, this function returns false, but out and outlen + * will still be populated with good defaults. + */ +bool git_repository__reserved_names( + git_str **out, size_t *outlen, git_repository *repo, bool include_ntfs); + +int git_repository__shallow_roots(git_oid **out, size_t *out_len, git_repository *repo); +int git_repository__shallow_roots_write(git_repository *repo, git_oidarray *roots); + +/* + * The default branch for the repository; the `init.defaultBranch` + * configuration option, if set, or `master` if it is not. + */ +int git_repository_initialbranch(git_str *out, git_repository *repo); + +/* + * Given a relative `path`, this makes it absolute based on the + * repository's working directory. This will perform validation + * to ensure that the path is not longer than MAX_PATH on Windows + * (unless `core.longpaths` is set in the repo config). + */ +int git_repository_workdir_path(git_str *out, git_repository *repo, const char *path); + +int git_repository__extensions(char ***out, size_t *out_len); +int git_repository__set_extensions(const char **extensions, size_t len); +void git_repository__free_extensions(void); + +/* + * Set the object format (OID type) for a repository; this will set + * both the configuration and the internal value for the oid type. + */ +int git_repository__set_objectformat( + git_repository *repo, + git_oid_t oid_type); + +#endif diff --git a/src/libgit2/reset.c b/src/libgit2/reset.c new file mode 100644 index 0000000..605c4af --- /dev/null +++ b/src/libgit2/reset.c @@ -0,0 +1,204 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "commit.h" +#include "tag.h" +#include "merge.h" +#include "diff.h" +#include "annotated_commit.h" +#include "git2/reset.h" +#include "git2/checkout.h" +#include "git2/merge.h" +#include "git2/refs.h" + +#define ERROR_MSG "Cannot perform reset" + +int git_reset_default( + git_repository *repo, + const git_object *target, + const git_strarray *pathspecs) +{ + git_object *commit = NULL; + git_tree *tree = NULL; + git_diff *diff = NULL; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + size_t i, max_i; + git_index_entry entry; + int error; + git_index *index = NULL; + + GIT_ASSERT_ARG(pathspecs && pathspecs->count > 0); + + memset(&entry, 0, sizeof(git_index_entry)); + + if ((error = git_repository_index(&index, repo)) < 0) + goto cleanup; + + if (target) { + if (git_object_owner(target) != repo) { + git_error_set(GIT_ERROR_OBJECT, + "%s_default - The given target does not belong to this repository.", ERROR_MSG); + return -1; + } + + if ((error = git_object_peel(&commit, target, GIT_OBJECT_COMMIT)) < 0 || + (error = git_commit_tree(&tree, (git_commit *)commit)) < 0) + goto cleanup; + } + + opts.pathspec = *pathspecs; + opts.flags = GIT_DIFF_REVERSE; + + if ((error = git_diff_tree_to_index( + &diff, repo, tree, index, &opts)) < 0) + goto cleanup; + + for (i = 0, max_i = git_diff_num_deltas(diff); i < max_i; ++i) { + const git_diff_delta *delta = git_diff_get_delta(diff, i); + + GIT_ASSERT(delta->status == GIT_DELTA_ADDED || + delta->status == GIT_DELTA_MODIFIED || + delta->status == GIT_DELTA_CONFLICTED || + delta->status == GIT_DELTA_DELETED); + + error = git_index_conflict_remove(index, delta->old_file.path); + if (error < 0) { + if (delta->status == GIT_DELTA_ADDED && error == GIT_ENOTFOUND) + git_error_clear(); + else + goto cleanup; + } + + if (delta->status == GIT_DELTA_DELETED) { + if ((error = git_index_remove(index, delta->old_file.path, 0)) < 0) + goto cleanup; + } else { + entry.mode = delta->new_file.mode; + git_oid_cpy(&entry.id, &delta->new_file.id); + entry.path = (char *)delta->new_file.path; + + if ((error = git_index_add(index, &entry)) < 0) + goto cleanup; + } + } + + error = git_index_write(index); + +cleanup: + git_object_free(commit); + git_tree_free(tree); + git_index_free(index); + git_diff_free(diff); + + return error; +} + +static int reset( + git_repository *repo, + const git_object *target, + const char *to, + git_reset_t reset_type, + const git_checkout_options *checkout_opts) +{ + git_object *commit = NULL; + git_index *index = NULL; + git_tree *tree = NULL; + int error = 0; + git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; + git_str log_message = GIT_STR_INIT; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(target); + + if (checkout_opts) + opts = *checkout_opts; + + if (git_object_owner(target) != repo) { + git_error_set(GIT_ERROR_OBJECT, + "%s - The given target does not belong to this repository.", ERROR_MSG); + return -1; + } + + if (reset_type != GIT_RESET_SOFT && + (error = git_repository__ensure_not_bare(repo, + reset_type == GIT_RESET_MIXED ? "reset mixed" : "reset hard")) < 0) + return error; + + if ((error = git_object_peel(&commit, target, GIT_OBJECT_COMMIT)) < 0 || + (error = git_repository_index(&index, repo)) < 0 || + (error = git_commit_tree(&tree, (git_commit *)commit)) < 0) + goto cleanup; + + if (reset_type == GIT_RESET_SOFT && + (git_repository_state(repo) == GIT_REPOSITORY_STATE_MERGE || + git_index_has_conflicts(index))) + { + git_error_set(GIT_ERROR_OBJECT, "%s (soft) in the middle of a merge", ERROR_MSG); + error = GIT_EUNMERGED; + goto cleanup; + } + + if ((error = git_str_printf(&log_message, "reset: moving to %s", to)) < 0) + return error; + + if (reset_type == GIT_RESET_HARD) { + /* overwrite working directory with the new tree */ + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + + if ((error = git_checkout_tree(repo, (git_object *)tree, &opts)) < 0) + goto cleanup; + } + + /* move HEAD to the new target */ + if ((error = git_reference__update_terminal(repo, GIT_HEAD_FILE, + git_object_id(commit), NULL, git_str_cstr(&log_message))) < 0) + goto cleanup; + + if (reset_type > GIT_RESET_SOFT) { + /* reset index to the target content */ + + if ((error = git_index_read_tree(index, tree)) < 0 || + (error = git_index_write(index)) < 0) + goto cleanup; + + if ((error = git_repository_state_cleanup(repo)) < 0) { + git_error_set(GIT_ERROR_INDEX, "%s - failed to clean up merge data", ERROR_MSG); + goto cleanup; + } + } + +cleanup: + git_object_free(commit); + git_index_free(index); + git_tree_free(tree); + git_str_dispose(&log_message); + + return error; +} + +int git_reset( + git_repository *repo, + const git_object *target, + git_reset_t reset_type, + const git_checkout_options *checkout_opts) +{ + char to[GIT_OID_MAX_HEXSIZE + 1]; + + git_oid_tostr(to, GIT_OID_MAX_HEXSIZE + 1, git_object_id(target)); + return reset(repo, target, to, reset_type, checkout_opts); +} + +int git_reset_from_annotated( + git_repository *repo, + const git_annotated_commit *commit, + git_reset_t reset_type, + const git_checkout_options *checkout_opts) +{ + return reset(repo, (git_object *) commit->commit, commit->description, reset_type, checkout_opts); +} diff --git a/src/libgit2/revert.c b/src/libgit2/revert.c new file mode 100644 index 0000000..4a31ad4 --- /dev/null +++ b/src/libgit2/revert.c @@ -0,0 +1,240 @@ +/* +* Copyright (C) the libgit2 contributors. All rights reserved. +* +* This file is part of libgit2, distributed under the GNU GPL v2 with +* a Linking Exception. For full terms see the included COPYING file. +*/ + +#include "common.h" + +#include "repository.h" +#include "filebuf.h" +#include "merge.h" +#include "index.h" + +#include "git2/types.h" +#include "git2/merge.h" +#include "git2/revert.h" +#include "git2/commit.h" +#include "git2/sys/commit.h" + +#define GIT_REVERT_FILE_MODE 0666 + +static int write_revert_head( + git_repository *repo, + const char *commit_oidstr) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_str file_path = GIT_STR_INIT; + int error = 0; + + if ((error = git_str_joinpath(&file_path, repo->gitdir, GIT_REVERT_HEAD_FILE)) >= 0 && + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_CREATE_LEADING_DIRS, GIT_REVERT_FILE_MODE)) >= 0 && + (error = git_filebuf_printf(&file, "%s\n", commit_oidstr)) >= 0) + error = git_filebuf_commit(&file); + + if (error < 0) + git_filebuf_cleanup(&file); + + git_str_dispose(&file_path); + + return error; +} + +static int write_merge_msg( + git_repository *repo, + const char *commit_oidstr, + const char *commit_msgline) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_str file_path = GIT_STR_INIT; + int error = 0; + + if ((error = git_str_joinpath(&file_path, repo->gitdir, GIT_MERGE_MSG_FILE)) < 0 || + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_CREATE_LEADING_DIRS, GIT_REVERT_FILE_MODE)) < 0 || + (error = git_filebuf_printf(&file, "Revert \"%s\"\n\nThis reverts commit %s.\n", + commit_msgline, commit_oidstr)) < 0) + goto cleanup; + + error = git_filebuf_commit(&file); + +cleanup: + if (error < 0) + git_filebuf_cleanup(&file); + + git_str_dispose(&file_path); + + return error; +} + +static int revert_normalize_opts( + git_repository *repo, + git_revert_options *opts, + const git_revert_options *given, + const char *their_label) +{ + int error = 0; + unsigned int default_checkout_strategy = GIT_CHECKOUT_SAFE | + GIT_CHECKOUT_ALLOW_CONFLICTS; + + GIT_UNUSED(repo); + + if (given != NULL) + memcpy(opts, given, sizeof(git_revert_options)); + else { + git_revert_options default_opts = GIT_REVERT_OPTIONS_INIT; + memcpy(opts, &default_opts, sizeof(git_revert_options)); + } + + if (!opts->checkout_opts.checkout_strategy) + opts->checkout_opts.checkout_strategy = default_checkout_strategy; + + if (!opts->checkout_opts.our_label) + opts->checkout_opts.our_label = "HEAD"; + + if (!opts->checkout_opts.their_label) + opts->checkout_opts.their_label = their_label; + + return error; +} + +static int revert_state_cleanup(git_repository *repo) +{ + const char *state_files[] = { GIT_REVERT_HEAD_FILE, GIT_MERGE_MSG_FILE }; + + return git_repository__cleanup_files(repo, state_files, ARRAY_SIZE(state_files)); +} + +static int revert_seterr(git_commit *commit, const char *fmt) +{ + char commit_id[GIT_OID_MAX_HEXSIZE + 1]; + + git_oid_tostr(commit_id, GIT_OID_MAX_HEXSIZE + 1, git_commit_id(commit)); + git_error_set(GIT_ERROR_REVERT, fmt, commit_id); + + return -1; +} + +int git_revert_commit( + git_index **out, + git_repository *repo, + git_commit *revert_commit, + git_commit *our_commit, + unsigned int mainline, + const git_merge_options *merge_opts) +{ + git_commit *parent_commit = NULL; + git_tree *parent_tree = NULL, *our_tree = NULL, *revert_tree = NULL; + int parent = 0, error = 0; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(revert_commit); + GIT_ASSERT_ARG(our_commit); + + if (git_commit_parentcount(revert_commit) > 1) { + if (!mainline) + return revert_seterr(revert_commit, + "mainline branch is not specified but %s is a merge commit"); + + parent = mainline; + } else { + if (mainline) + return revert_seterr(revert_commit, + "mainline branch specified but %s is not a merge commit"); + + parent = git_commit_parentcount(revert_commit); + } + + if (parent && + ((error = git_commit_parent(&parent_commit, revert_commit, (parent - 1))) < 0 || + (error = git_commit_tree(&parent_tree, parent_commit)) < 0)) + goto done; + + if ((error = git_commit_tree(&revert_tree, revert_commit)) < 0 || + (error = git_commit_tree(&our_tree, our_commit)) < 0) + goto done; + + error = git_merge_trees(out, repo, revert_tree, our_tree, parent_tree, merge_opts); + +done: + git_tree_free(parent_tree); + git_tree_free(our_tree); + git_tree_free(revert_tree); + git_commit_free(parent_commit); + + return error; +} + +int git_revert( + git_repository *repo, + git_commit *commit, + const git_revert_options *given_opts) +{ + git_revert_options opts; + git_reference *our_ref = NULL; + git_commit *our_commit = NULL; + char commit_id[GIT_OID_MAX_HEXSIZE + 1]; + const char *commit_msg; + git_str their_label = GIT_STR_INIT; + git_index *index = NULL; + git_indexwriter indexwriter = GIT_INDEXWRITER_INIT; + int error; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(commit); + + GIT_ERROR_CHECK_VERSION(given_opts, GIT_REVERT_OPTIONS_VERSION, "git_revert_options"); + + if ((error = git_repository__ensure_not_bare(repo, "revert")) < 0) + return error; + + git_oid_tostr(commit_id, GIT_OID_MAX_HEXSIZE + 1, git_commit_id(commit)); + + if ((commit_msg = git_commit_summary(commit)) == NULL) { + error = -1; + goto on_error; + } + + if ((error = git_str_printf(&their_label, "parent of %.7s... %s", commit_id, commit_msg)) < 0 || + (error = revert_normalize_opts(repo, &opts, given_opts, git_str_cstr(&their_label))) < 0 || + (error = git_indexwriter_init_for_operation(&indexwriter, repo, &opts.checkout_opts.checkout_strategy)) < 0 || + (error = write_revert_head(repo, commit_id)) < 0 || + (error = write_merge_msg(repo, commit_id, commit_msg)) < 0 || + (error = git_repository_head(&our_ref, repo)) < 0 || + (error = git_reference_peel((git_object **)&our_commit, our_ref, GIT_OBJECT_COMMIT)) < 0 || + (error = git_revert_commit(&index, repo, commit, our_commit, opts.mainline, &opts.merge_opts)) < 0 || + (error = git_merge__check_result(repo, index)) < 0 || + (error = git_merge__append_conflicts_to_merge_msg(repo, index)) < 0 || + (error = git_checkout_index(repo, index, &opts.checkout_opts)) < 0 || + (error = git_indexwriter_commit(&indexwriter)) < 0) + goto on_error; + + goto done; + +on_error: + revert_state_cleanup(repo); + +done: + git_indexwriter_cleanup(&indexwriter); + git_index_free(index); + git_commit_free(our_commit); + git_reference_free(our_ref); + git_str_dispose(&their_label); + + return error; +} + +int git_revert_options_init(git_revert_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_revert_options, GIT_REVERT_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_revert_init_options(git_revert_options *opts, unsigned int version) +{ + return git_revert_options_init(opts, version); +} +#endif diff --git a/src/libgit2/revparse.c b/src/libgit2/revparse.c new file mode 100644 index 0000000..06d92f8 --- /dev/null +++ b/src/libgit2/revparse.c @@ -0,0 +1,968 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "str.h" +#include "tree.h" +#include "refdb.h" +#include "regexp.h" +#include "date.h" + +#include "git2.h" + +static int maybe_sha_or_abbrev( + git_object **out, + git_repository *repo, + const char *spec, + size_t speclen) +{ + git_oid oid; + + if (git_oid__fromstrn(&oid, spec, speclen, repo->oid_type) < 0) + return GIT_ENOTFOUND; + + return git_object_lookup_prefix(out, repo, &oid, speclen, GIT_OBJECT_ANY); +} + +static int maybe_sha( + git_object **out, + git_repository *repo, + const char *spec) +{ + size_t speclen = strlen(spec); + + if (speclen != git_oid_hexsize(repo->oid_type)) + return GIT_ENOTFOUND; + + return maybe_sha_or_abbrev(out, repo, spec, speclen); +} + +static int maybe_abbrev(git_object **out, git_repository *repo, const char *spec) +{ + size_t speclen = strlen(spec); + + return maybe_sha_or_abbrev(out, repo, spec, speclen); +} + +static int build_regex(git_regexp *regex, const char *pattern) +{ + int error; + + if (*pattern == '\0') { + git_error_set(GIT_ERROR_REGEX, "empty pattern"); + return GIT_EINVALIDSPEC; + } + + error = git_regexp_compile(regex, pattern, 0); + if (!error) + return 0; + + git_regexp_dispose(regex); + + return error; +} + +static int maybe_describe(git_object**out, git_repository *repo, const char *spec) +{ + const char *substr; + int error; + git_regexp regex; + + substr = strstr(spec, "-g"); + + if (substr == NULL) + return GIT_ENOTFOUND; + + if (build_regex(®ex, ".+-[0-9]+-g[0-9a-fA-F]+") < 0) + return -1; + + error = git_regexp_match(®ex, spec); + git_regexp_dispose(®ex); + + if (error) + return GIT_ENOTFOUND; + + return maybe_abbrev(out, repo, substr+2); +} + +static int revparse_lookup_object( + git_object **object_out, + git_reference **reference_out, + git_repository *repo, + const char *spec) +{ + int error; + git_reference *ref; + + if ((error = maybe_sha(object_out, repo, spec)) != GIT_ENOTFOUND) + return error; + + error = git_reference_dwim(&ref, repo, spec); + if (!error) { + + error = git_object_lookup( + object_out, repo, git_reference_target(ref), GIT_OBJECT_ANY); + + if (!error) + *reference_out = ref; + + return error; + } + + if (error != GIT_ENOTFOUND) + return error; + + if ((strlen(spec) < git_oid_hexsize(repo->oid_type)) && + ((error = maybe_abbrev(object_out, repo, spec)) != GIT_ENOTFOUND)) + return error; + + if ((error = maybe_describe(object_out, repo, spec)) != GIT_ENOTFOUND) + return error; + + git_error_set(GIT_ERROR_REFERENCE, "revspec '%s' not found", spec); + return GIT_ENOTFOUND; +} + +static int try_parse_numeric(int *n, const char *curly_braces_content) +{ + int32_t content; + const char *end_ptr; + + if (git__strntol32(&content, curly_braces_content, strlen(curly_braces_content), + &end_ptr, 10) < 0) + return -1; + + if (*end_ptr != '\0') + return -1; + + *n = (int)content; + return 0; +} + +static int retrieve_previously_checked_out_branch_or_revision(git_object **out, git_reference **base_ref, git_repository *repo, const char *identifier, size_t position) +{ + git_reference *ref = NULL; + git_reflog *reflog = NULL; + git_regexp preg; + int error = -1; + size_t i, numentries, cur; + const git_reflog_entry *entry; + const char *msg; + git_str buf = GIT_STR_INIT; + + cur = position; + + if (*identifier != '\0' || *base_ref != NULL) + return GIT_EINVALIDSPEC; + + if (build_regex(&preg, "checkout: moving from (.*) to .*") < 0) + return -1; + + if (git_reference_lookup(&ref, repo, GIT_HEAD_FILE) < 0) + goto cleanup; + + if (git_reflog_read(&reflog, repo, GIT_HEAD_FILE) < 0) + goto cleanup; + + numentries = git_reflog_entrycount(reflog); + + for (i = 0; i < numentries; i++) { + git_regmatch regexmatches[2]; + + entry = git_reflog_entry_byindex(reflog, i); + msg = git_reflog_entry_message(entry); + if (!msg) + continue; + + if (git_regexp_search(&preg, msg, 2, regexmatches) < 0) + continue; + + cur--; + + if (cur > 0) + continue; + + if ((git_str_put(&buf, msg+regexmatches[1].start, regexmatches[1].end - regexmatches[1].start)) < 0) + goto cleanup; + + if ((error = git_reference_dwim(base_ref, repo, git_str_cstr(&buf))) == 0) + goto cleanup; + + if (error < 0 && error != GIT_ENOTFOUND) + goto cleanup; + + error = maybe_abbrev(out, repo, git_str_cstr(&buf)); + + goto cleanup; + } + + error = GIT_ENOTFOUND; + +cleanup: + git_reference_free(ref); + git_str_dispose(&buf); + git_regexp_dispose(&preg); + git_reflog_free(reflog); + return error; +} + +static int retrieve_oid_from_reflog(git_oid *oid, git_reference *ref, size_t identifier) +{ + git_reflog *reflog; + size_t numentries; + const git_reflog_entry *entry = NULL; + bool search_by_pos = (identifier <= 100000000); + + if (git_reflog_read(&reflog, git_reference_owner(ref), git_reference_name(ref)) < 0) + return -1; + + numentries = git_reflog_entrycount(reflog); + + if (search_by_pos) { + if (numentries < identifier + 1) + goto notfound; + + entry = git_reflog_entry_byindex(reflog, identifier); + git_oid_cpy(oid, git_reflog_entry_id_new(entry)); + } else { + size_t i; + git_time commit_time; + + for (i = 0; i < numentries; i++) { + entry = git_reflog_entry_byindex(reflog, i); + commit_time = git_reflog_entry_committer(entry)->when; + + if (commit_time.time > (git_time_t)identifier) + continue; + + git_oid_cpy(oid, git_reflog_entry_id_new(entry)); + break; + } + + if (i == numentries) { + if (entry == NULL) + goto notfound; + + /* + * TODO: emit a warning (log for 'branch' only goes back to ...) + */ + git_oid_cpy(oid, git_reflog_entry_id_new(entry)); + } + } + + git_reflog_free(reflog); + return 0; + +notfound: + git_error_set( + GIT_ERROR_REFERENCE, + "reflog for '%s' has only %"PRIuZ" entries, asked for %"PRIuZ, + git_reference_name(ref), numentries, identifier); + + git_reflog_free(reflog); + return GIT_ENOTFOUND; +} + +static int retrieve_revobject_from_reflog(git_object **out, git_reference **base_ref, git_repository *repo, const char *identifier, size_t position) +{ + git_reference *ref; + git_oid oid; + int error = -1; + + if (*base_ref == NULL) { + /* + * When HEAD@{n} is specified, do not use dwim, which would resolve the + * reference (to the current branch that HEAD is pointing to). + */ + if (position > 0 && strcmp(identifier, GIT_HEAD_FILE) == 0) + error = git_reference_lookup(&ref, repo, GIT_HEAD_FILE); + else + error = git_reference_dwim(&ref, repo, identifier); + + if (error < 0) + return error; + } else { + ref = *base_ref; + *base_ref = NULL; + } + + if (position == 0) { + error = git_object_lookup(out, repo, git_reference_target(ref), GIT_OBJECT_ANY); + goto cleanup; + } + + if ((error = retrieve_oid_from_reflog(&oid, ref, position)) < 0) + goto cleanup; + + error = git_object_lookup(out, repo, &oid, GIT_OBJECT_ANY); + +cleanup: + git_reference_free(ref); + return error; +} + +static int retrieve_remote_tracking_reference(git_reference **base_ref, const char *identifier, git_repository *repo) +{ + git_reference *tracking, *ref; + int error = -1; + + if (*base_ref == NULL) { + if ((error = git_reference_dwim(&ref, repo, identifier)) < 0) + return error; + } else { + ref = *base_ref; + *base_ref = NULL; + } + + if (!git_reference_is_branch(ref)) { + error = GIT_EINVALIDSPEC; + goto cleanup; + } + + if ((error = git_branch_upstream(&tracking, ref)) < 0) + goto cleanup; + + *base_ref = tracking; + +cleanup: + git_reference_free(ref); + return error; +} + +static int handle_at_syntax(git_object **out, git_reference **ref, const char *spec, size_t identifier_len, git_repository *repo, const char *curly_braces_content) +{ + bool is_numeric; + int parsed = 0, error = -1; + git_str identifier = GIT_STR_INIT; + git_time_t timestamp; + + GIT_ASSERT(*out == NULL); + + if (git_str_put(&identifier, spec, identifier_len) < 0) + return -1; + + is_numeric = !try_parse_numeric(&parsed, curly_braces_content); + + if (*curly_braces_content == '-' && (!is_numeric || parsed == 0)) { + error = GIT_EINVALIDSPEC; + goto cleanup; + } + + if (is_numeric) { + if (parsed < 0) + error = retrieve_previously_checked_out_branch_or_revision(out, ref, repo, git_str_cstr(&identifier), -parsed); + else + error = retrieve_revobject_from_reflog(out, ref, repo, git_str_cstr(&identifier), parsed); + + goto cleanup; + } + + if (!strcmp(curly_braces_content, "u") || !strcmp(curly_braces_content, "upstream")) { + error = retrieve_remote_tracking_reference(ref, git_str_cstr(&identifier), repo); + + goto cleanup; + } + + if (git_date_parse(×tamp, curly_braces_content) < 0) { + error = GIT_EINVALIDSPEC; + goto cleanup; + } + + error = retrieve_revobject_from_reflog(out, ref, repo, git_str_cstr(&identifier), (size_t)timestamp); + +cleanup: + git_str_dispose(&identifier); + return error; +} + +static git_object_t parse_obj_type(const char *str) +{ + if (!strcmp(str, "commit")) + return GIT_OBJECT_COMMIT; + + if (!strcmp(str, "tree")) + return GIT_OBJECT_TREE; + + if (!strcmp(str, "blob")) + return GIT_OBJECT_BLOB; + + if (!strcmp(str, "tag")) + return GIT_OBJECT_TAG; + + return GIT_OBJECT_INVALID; +} + +static int dereference_to_non_tag(git_object **out, git_object *obj) +{ + if (git_object_type(obj) == GIT_OBJECT_TAG) + return git_tag_peel(out, (git_tag *)obj); + + return git_object_dup(out, obj); +} + +static int handle_caret_parent_syntax(git_object **out, git_object *obj, int n) +{ + git_object *temp_commit = NULL; + int error; + + if ((error = git_object_peel(&temp_commit, obj, GIT_OBJECT_COMMIT)) < 0) + return (error == GIT_EAMBIGUOUS || error == GIT_ENOTFOUND) ? + GIT_EINVALIDSPEC : error; + + if (n == 0) { + *out = temp_commit; + return 0; + } + + error = git_commit_parent((git_commit **)out, (git_commit*)temp_commit, n - 1); + + git_object_free(temp_commit); + return error; +} + +static int handle_linear_syntax(git_object **out, git_object *obj, int n) +{ + git_object *temp_commit = NULL; + int error; + + if ((error = git_object_peel(&temp_commit, obj, GIT_OBJECT_COMMIT)) < 0) + return (error == GIT_EAMBIGUOUS || error == GIT_ENOTFOUND) ? + GIT_EINVALIDSPEC : error; + + error = git_commit_nth_gen_ancestor((git_commit **)out, (git_commit*)temp_commit, n); + + git_object_free(temp_commit); + return error; +} + +static int handle_colon_syntax( + git_object **out, + git_object *obj, + const char *path) +{ + git_object *tree; + int error = -1; + git_tree_entry *entry = NULL; + + if ((error = git_object_peel(&tree, obj, GIT_OBJECT_TREE)) < 0) + return error == GIT_ENOTFOUND ? GIT_EINVALIDSPEC : error; + + if (*path == '\0') { + *out = tree; + return 0; + } + + /* + * TODO: Handle the relative path syntax + * (:./relative/path and :../relative/path) + */ + if ((error = git_tree_entry_bypath(&entry, (git_tree *)tree, path)) < 0) + goto cleanup; + + error = git_tree_entry_to_object(out, git_object_owner(tree), entry); + +cleanup: + git_tree_entry_free(entry); + git_object_free(tree); + + return error; +} + +static int walk_and_search(git_object **out, git_revwalk *walk, git_regexp *regex) +{ + int error; + git_oid oid; + git_object *obj; + + while (!(error = git_revwalk_next(&oid, walk))) { + + error = git_object_lookup(&obj, git_revwalk_repository(walk), &oid, GIT_OBJECT_COMMIT); + if ((error < 0) && (error != GIT_ENOTFOUND)) + return -1; + + if (!git_regexp_match(regex, git_commit_message((git_commit*)obj))) { + *out = obj; + return 0; + } + + git_object_free(obj); + } + + if (error < 0 && error == GIT_ITEROVER) + error = GIT_ENOTFOUND; + + return error; +} + +static int handle_grep_syntax(git_object **out, git_repository *repo, const git_oid *spec_oid, const char *pattern) +{ + git_regexp preg; + git_revwalk *walk = NULL; + int error; + + if ((error = build_regex(&preg, pattern)) < 0) + return error; + + if ((error = git_revwalk_new(&walk, repo)) < 0) + goto cleanup; + + git_revwalk_sorting(walk, GIT_SORT_TIME); + + if (spec_oid == NULL) { + if ((error = git_revwalk_push_glob(walk, "refs/*")) < 0) + goto cleanup; + } else if ((error = git_revwalk_push(walk, spec_oid)) < 0) + goto cleanup; + + error = walk_and_search(out, walk, &preg); + +cleanup: + git_regexp_dispose(&preg); + git_revwalk_free(walk); + + return error; +} + +static int handle_caret_curly_syntax(git_object **out, git_object *obj, const char *curly_braces_content) +{ + git_object_t expected_type; + + if (*curly_braces_content == '\0') + return dereference_to_non_tag(out, obj); + + if (*curly_braces_content == '/') + return handle_grep_syntax(out, git_object_owner(obj), git_object_id(obj), curly_braces_content + 1); + + expected_type = parse_obj_type(curly_braces_content); + + if (expected_type == GIT_OBJECT_INVALID) + return GIT_EINVALIDSPEC; + + return git_object_peel(out, obj, expected_type); +} + +static int extract_curly_braces_content(git_str *buf, const char *spec, size_t *pos) +{ + git_str_clear(buf); + + GIT_ASSERT_ARG(spec[*pos] == '^' || spec[*pos] == '@'); + + (*pos)++; + + if (spec[*pos] == '\0' || spec[*pos] != '{') + return GIT_EINVALIDSPEC; + + (*pos)++; + + while (spec[*pos] != '}') { + if (spec[*pos] == '\0') + return GIT_EINVALIDSPEC; + + if (git_str_putc(buf, spec[(*pos)++]) < 0) + return -1; + } + + (*pos)++; + + return 0; +} + +static int extract_path(git_str *buf, const char *spec, size_t *pos) +{ + git_str_clear(buf); + + GIT_ASSERT_ARG(spec[*pos] == ':'); + + (*pos)++; + + if (git_str_puts(buf, spec + *pos) < 0) + return -1; + + *pos += git_str_len(buf); + + return 0; +} + +static int extract_how_many(int *n, const char *spec, size_t *pos) +{ + const char *end_ptr; + int parsed, accumulated; + char kind = spec[*pos]; + + GIT_ASSERT_ARG(spec[*pos] == '^' || spec[*pos] == '~'); + + accumulated = 0; + + do { + do { + (*pos)++; + accumulated++; + } while (spec[(*pos)] == kind && kind == '~'); + + if (git__isdigit(spec[*pos])) { + if (git__strntol32(&parsed, spec + *pos, strlen(spec + *pos), &end_ptr, 10) < 0) + return GIT_EINVALIDSPEC; + + accumulated += (parsed - 1); + *pos = end_ptr - spec; + } + + } while (spec[(*pos)] == kind && kind == '~'); + + *n = accumulated; + + return 0; +} + +static int object_from_reference(git_object **object, git_reference *reference) +{ + git_reference *resolved = NULL; + int error; + + if (git_reference_resolve(&resolved, reference) < 0) + return -1; + + error = git_object_lookup(object, reference->db->repo, git_reference_target(resolved), GIT_OBJECT_ANY); + git_reference_free(resolved); + + return error; +} + +static int ensure_base_rev_loaded(git_object **object, git_reference **reference, const char *spec, size_t identifier_len, git_repository *repo, bool allow_empty_identifier) +{ + int error; + git_str identifier = GIT_STR_INIT; + + if (*object != NULL) + return 0; + + if (*reference != NULL) + return object_from_reference(object, *reference); + + if (!allow_empty_identifier && identifier_len == 0) + return GIT_EINVALIDSPEC; + + if (git_str_put(&identifier, spec, identifier_len) < 0) + return -1; + + error = revparse_lookup_object(object, reference, repo, git_str_cstr(&identifier)); + git_str_dispose(&identifier); + + return error; +} + +static int ensure_base_rev_is_not_known_yet(git_object *object) +{ + if (object == NULL) + return 0; + + return GIT_EINVALIDSPEC; +} + +static bool any_left_hand_identifier(git_object *object, git_reference *reference, size_t identifier_len) +{ + if (object != NULL) + return true; + + if (reference != NULL) + return true; + + if (identifier_len > 0) + return true; + + return false; +} + +static int ensure_left_hand_identifier_is_not_known_yet(git_object *object, git_reference *reference) +{ + if (!ensure_base_rev_is_not_known_yet(object) && reference == NULL) + return 0; + + return GIT_EINVALIDSPEC; +} + +static int revparse( + git_object **object_out, + git_reference **reference_out, + size_t *identifier_len_out, + git_repository *repo, + const char *spec) +{ + size_t pos = 0, identifier_len = 0; + int error = -1, n; + git_str buf = GIT_STR_INIT; + + git_reference *reference = NULL; + git_object *base_rev = NULL; + + bool should_return_reference = true; + bool parsed = false; + + GIT_ASSERT_ARG(object_out); + GIT_ASSERT_ARG(reference_out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(spec); + + *object_out = NULL; + *reference_out = NULL; + + while (!parsed && spec[pos]) { + switch (spec[pos]) { + case '^': + should_return_reference = false; + + if ((error = ensure_base_rev_loaded(&base_rev, &reference, spec, identifier_len, repo, false)) < 0) + goto cleanup; + + if (spec[pos+1] == '{') { + git_object *temp_object = NULL; + + if ((error = extract_curly_braces_content(&buf, spec, &pos)) < 0) + goto cleanup; + + if ((error = handle_caret_curly_syntax(&temp_object, base_rev, git_str_cstr(&buf))) < 0) + goto cleanup; + + git_object_free(base_rev); + base_rev = temp_object; + } else { + git_object *temp_object = NULL; + + if ((error = extract_how_many(&n, spec, &pos)) < 0) + goto cleanup; + + if ((error = handle_caret_parent_syntax(&temp_object, base_rev, n)) < 0) + goto cleanup; + + git_object_free(base_rev); + base_rev = temp_object; + } + break; + + case '~': + { + git_object *temp_object = NULL; + + should_return_reference = false; + + if ((error = extract_how_many(&n, spec, &pos)) < 0) + goto cleanup; + + if ((error = ensure_base_rev_loaded(&base_rev, &reference, spec, identifier_len, repo, false)) < 0) + goto cleanup; + + if ((error = handle_linear_syntax(&temp_object, base_rev, n)) < 0) + goto cleanup; + + git_object_free(base_rev); + base_rev = temp_object; + break; + } + + case ':': + { + git_object *temp_object = NULL; + + should_return_reference = false; + + if ((error = extract_path(&buf, spec, &pos)) < 0) + goto cleanup; + + if (any_left_hand_identifier(base_rev, reference, identifier_len)) { + if ((error = ensure_base_rev_loaded(&base_rev, &reference, spec, identifier_len, repo, true)) < 0) + goto cleanup; + + if ((error = handle_colon_syntax(&temp_object, base_rev, git_str_cstr(&buf))) < 0) + goto cleanup; + } else { + if (*git_str_cstr(&buf) == '/') { + if ((error = handle_grep_syntax(&temp_object, repo, NULL, git_str_cstr(&buf) + 1)) < 0) + goto cleanup; + } else { + + /* + * TODO: support merge-stage path lookup (":2:Makefile") + * and plain index blob lookup (:i-am/a/blob) + */ + git_error_set(GIT_ERROR_INVALID, "unimplemented"); + error = GIT_ERROR; + goto cleanup; + } + } + + git_object_free(base_rev); + base_rev = temp_object; + break; + } + + case '@': + if (spec[pos+1] == '{') { + git_object *temp_object = NULL; + + if ((error = extract_curly_braces_content(&buf, spec, &pos)) < 0) + goto cleanup; + + if ((error = ensure_base_rev_is_not_known_yet(base_rev)) < 0) + goto cleanup; + + if ((error = handle_at_syntax(&temp_object, &reference, spec, identifier_len, repo, git_str_cstr(&buf))) < 0) + goto cleanup; + + if (temp_object != NULL) + base_rev = temp_object; + break; + } else if (spec[pos+1] == '\0') { + spec = "HEAD"; + identifier_len = 4; + parsed = true; + break; + } + /* fall through */ + + default: + if ((error = ensure_left_hand_identifier_is_not_known_yet(base_rev, reference)) < 0) + goto cleanup; + + pos++; + identifier_len++; + } + } + + if ((error = ensure_base_rev_loaded(&base_rev, &reference, spec, identifier_len, repo, false)) < 0) + goto cleanup; + + if (!should_return_reference) { + git_reference_free(reference); + reference = NULL; + } + + *object_out = base_rev; + *reference_out = reference; + *identifier_len_out = identifier_len; + error = 0; + +cleanup: + if (error) { + if (error == GIT_EINVALIDSPEC) + git_error_set(GIT_ERROR_INVALID, + "failed to parse revision specifier - Invalid pattern '%s'", spec); + + git_object_free(base_rev); + git_reference_free(reference); + } + + git_str_dispose(&buf); + return error; +} + +int git_revparse_ext( + git_object **object_out, + git_reference **reference_out, + git_repository *repo, + const char *spec) +{ + int error; + size_t identifier_len; + git_object *obj = NULL; + git_reference *ref = NULL; + + if ((error = revparse(&obj, &ref, &identifier_len, repo, spec)) < 0) + goto cleanup; + + *object_out = obj; + *reference_out = ref; + GIT_UNUSED(identifier_len); + + return 0; + +cleanup: + git_object_free(obj); + git_reference_free(ref); + return error; +} + +int git_revparse_single(git_object **out, git_repository *repo, const char *spec) +{ + int error; + git_object *obj = NULL; + git_reference *ref = NULL; + + *out = NULL; + + if ((error = git_revparse_ext(&obj, &ref, repo, spec)) < 0) + goto cleanup; + + git_reference_free(ref); + + *out = obj; + + return 0; + +cleanup: + git_object_free(obj); + git_reference_free(ref); + return error; +} + +int git_revparse( + git_revspec *revspec, + git_repository *repo, + const char *spec) +{ + const char *dotdot; + int error = 0; + + GIT_ASSERT_ARG(revspec); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(spec); + + memset(revspec, 0x0, sizeof(*revspec)); + + if ((dotdot = strstr(spec, "..")) != NULL) { + char *lstr; + const char *rstr; + revspec->flags = GIT_REVSPEC_RANGE; + + /* + * Following git.git, don't allow '..' because it makes command line + * arguments which can be either paths or revisions ambiguous when the + * path is almost certainly intended. The empty range '...' is still + * allowed. + */ + if (!git__strcmp(spec, "..")) { + git_error_set(GIT_ERROR_INVALID, "Invalid pattern '..'"); + return GIT_EINVALIDSPEC; + } + + lstr = git__substrdup(spec, dotdot - spec); + rstr = dotdot + 2; + if (dotdot[2] == '.') { + revspec->flags |= GIT_REVSPEC_MERGE_BASE; + rstr++; + } + + error = git_revparse_single( + &revspec->from, + repo, + *lstr == '\0' ? "HEAD" : lstr); + + if (!error) { + error = git_revparse_single( + &revspec->to, + repo, + *rstr == '\0' ? "HEAD" : rstr); + } + + git__free((void*)lstr); + } else { + revspec->flags = GIT_REVSPEC_SINGLE; + error = git_revparse_single(&revspec->from, repo, spec); + } + + return error; +} diff --git a/src/libgit2/revwalk.c b/src/libgit2/revwalk.c new file mode 100644 index 0000000..4ea6fae --- /dev/null +++ b/src/libgit2/revwalk.c @@ -0,0 +1,846 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "revwalk.h" + +#include "commit.h" +#include "odb.h" +#include "pool.h" + +#include "git2/revparse.h" +#include "merge.h" +#include "vector.h" + +static int get_revision(git_commit_list_node **out, git_revwalk *walk, git_commit_list **list); + +git_commit_list_node *git_revwalk__commit_lookup( + git_revwalk *walk, const git_oid *oid) +{ + git_commit_list_node *commit; + + /* lookup and reserve space if not already present */ + if ((commit = git_oidmap_get(walk->commits, oid)) != NULL) + return commit; + + commit = git_commit_list_alloc_node(walk); + if (commit == NULL) + return NULL; + + git_oid_cpy(&commit->oid, oid); + + if ((git_oidmap_set(walk->commits, &commit->oid, commit)) < 0) + return NULL; + + return commit; +} + +int git_revwalk__push_commit(git_revwalk *walk, const git_oid *oid, const git_revwalk__push_options *opts) +{ + git_oid commit_id; + int error; + git_object *obj, *oobj; + git_commit_list_node *commit; + git_commit_list *list; + + if ((error = git_object_lookup(&oobj, walk->repo, oid, GIT_OBJECT_ANY)) < 0) + return error; + + error = git_object_peel(&obj, oobj, GIT_OBJECT_COMMIT); + git_object_free(oobj); + + if (error == GIT_ENOTFOUND || error == GIT_EINVALIDSPEC || error == GIT_EPEEL) { + /* If this comes from e.g. push_glob("tags"), ignore this */ + if (opts->from_glob) + return 0; + + git_error_set(GIT_ERROR_INVALID, "object is not a committish"); + return error; + } + if (error < 0) + return error; + + git_oid_cpy(&commit_id, git_object_id(obj)); + git_object_free(obj); + + commit = git_revwalk__commit_lookup(walk, &commit_id); + if (commit == NULL) + return -1; /* error already reported by failed lookup */ + + /* A previous hide already told us we don't want this commit */ + if (commit->uninteresting) + return 0; + + if (opts->uninteresting) { + walk->limited = 1; + walk->did_hide = 1; + } else { + walk->did_push = 1; + } + + commit->uninteresting = opts->uninteresting; + list = walk->user_input; + + /* To insert by date, we need to parse so we know the date. */ + if (opts->insert_by_date && ((error = git_commit_list_parse(walk, commit)) < 0)) + return error; + + if ((opts->insert_by_date == 0 || + git_commit_list_insert_by_date(commit, &list) == NULL) && + git_commit_list_insert(commit, &list) == NULL) { + git_error_set_oom(); + return -1; + } + + walk->user_input = list; + + return 0; +} + +int git_revwalk_push(git_revwalk *walk, const git_oid *oid) +{ + git_revwalk__push_options opts = GIT_REVWALK__PUSH_OPTIONS_INIT; + + GIT_ASSERT_ARG(walk); + GIT_ASSERT_ARG(oid); + + return git_revwalk__push_commit(walk, oid, &opts); +} + + +int git_revwalk_hide(git_revwalk *walk, const git_oid *oid) +{ + git_revwalk__push_options opts = GIT_REVWALK__PUSH_OPTIONS_INIT; + + GIT_ASSERT_ARG(walk); + GIT_ASSERT_ARG(oid); + + opts.uninteresting = 1; + return git_revwalk__push_commit(walk, oid, &opts); +} + +int git_revwalk__push_ref(git_revwalk *walk, const char *refname, const git_revwalk__push_options *opts) +{ + git_oid oid; + + int error = git_reference_name_to_id(&oid, walk->repo, refname); + if (opts->from_glob && (error == GIT_ENOTFOUND || error == GIT_EINVALIDSPEC || error == GIT_EPEEL)) { + return 0; + } else if (error < 0) { + return -1; + } + + return git_revwalk__push_commit(walk, &oid, opts); +} + +int git_revwalk__push_glob(git_revwalk *walk, const char *glob, const git_revwalk__push_options *given_opts) +{ + git_revwalk__push_options opts = GIT_REVWALK__PUSH_OPTIONS_INIT; + int error = 0; + git_str buf = GIT_STR_INIT; + git_reference *ref; + git_reference_iterator *iter; + size_t wildcard; + + GIT_ASSERT_ARG(walk); + GIT_ASSERT_ARG(glob); + + if (given_opts) + memcpy(&opts, given_opts, sizeof(opts)); + + /* refs/ is implied if not given in the glob */ + if (git__prefixcmp(glob, GIT_REFS_DIR) != 0) + git_str_joinpath(&buf, GIT_REFS_DIR, glob); + else + git_str_puts(&buf, glob); + GIT_ERROR_CHECK_ALLOC_STR(&buf); + + /* If no '?', '*' or '[' exist, we append '/ *' to the glob */ + wildcard = strcspn(glob, "?*["); + if (!glob[wildcard]) + git_str_put(&buf, "/*", 2); + + if ((error = git_reference_iterator_glob_new(&iter, walk->repo, buf.ptr)) < 0) + goto out; + + opts.from_glob = true; + while ((error = git_reference_next(&ref, iter)) == 0) { + error = git_revwalk__push_ref(walk, git_reference_name(ref), &opts); + git_reference_free(ref); + if (error < 0) + break; + } + git_reference_iterator_free(iter); + + if (error == GIT_ITEROVER) + error = 0; +out: + git_str_dispose(&buf); + return error; +} + +int git_revwalk_push_glob(git_revwalk *walk, const char *glob) +{ + git_revwalk__push_options opts = GIT_REVWALK__PUSH_OPTIONS_INIT; + + GIT_ASSERT_ARG(walk); + GIT_ASSERT_ARG(glob); + + return git_revwalk__push_glob(walk, glob, &opts); +} + +int git_revwalk_hide_glob(git_revwalk *walk, const char *glob) +{ + git_revwalk__push_options opts = GIT_REVWALK__PUSH_OPTIONS_INIT; + + GIT_ASSERT_ARG(walk); + GIT_ASSERT_ARG(glob); + + opts.uninteresting = 1; + return git_revwalk__push_glob(walk, glob, &opts); +} + +int git_revwalk_push_head(git_revwalk *walk) +{ + git_revwalk__push_options opts = GIT_REVWALK__PUSH_OPTIONS_INIT; + + GIT_ASSERT_ARG(walk); + + return git_revwalk__push_ref(walk, GIT_HEAD_FILE, &opts); +} + +int git_revwalk_hide_head(git_revwalk *walk) +{ + git_revwalk__push_options opts = GIT_REVWALK__PUSH_OPTIONS_INIT; + + GIT_ASSERT_ARG(walk); + + opts.uninteresting = 1; + return git_revwalk__push_ref(walk, GIT_HEAD_FILE, &opts); +} + +int git_revwalk_push_ref(git_revwalk *walk, const char *refname) +{ + git_revwalk__push_options opts = GIT_REVWALK__PUSH_OPTIONS_INIT; + + GIT_ASSERT_ARG(walk); + GIT_ASSERT_ARG(refname); + + return git_revwalk__push_ref(walk, refname, &opts); +} + +int git_revwalk_push_range(git_revwalk *walk, const char *range) +{ + git_revwalk__push_options opts = GIT_REVWALK__PUSH_OPTIONS_INIT; + git_revspec revspec; + int error = 0; + + if ((error = git_revparse(&revspec, walk->repo, range))) + return error; + + if (!revspec.to) { + git_error_set(GIT_ERROR_INVALID, "invalid revspec: range not provided"); + error = GIT_EINVALIDSPEC; + goto out; + } + + if (revspec.flags & GIT_REVSPEC_MERGE_BASE) { + /* TODO: support "..." */ + git_error_set(GIT_ERROR_INVALID, "symmetric differences not implemented in revwalk"); + error = GIT_EINVALIDSPEC; + goto out; + } + + opts.uninteresting = 1; + if ((error = git_revwalk__push_commit(walk, git_object_id(revspec.from), &opts))) + goto out; + + opts.uninteresting = 0; + error = git_revwalk__push_commit(walk, git_object_id(revspec.to), &opts); + +out: + git_object_free(revspec.from); + git_object_free(revspec.to); + return error; +} + +int git_revwalk_hide_ref(git_revwalk *walk, const char *refname) +{ + git_revwalk__push_options opts = GIT_REVWALK__PUSH_OPTIONS_INIT; + + GIT_ASSERT_ARG(walk); + GIT_ASSERT_ARG(refname); + + opts.uninteresting = 1; + return git_revwalk__push_ref(walk, refname, &opts); +} + +static int revwalk_enqueue_timesort(git_revwalk *walk, git_commit_list_node *commit) +{ + return git_pqueue_insert(&walk->iterator_time, commit); +} + +static int revwalk_enqueue_unsorted(git_revwalk *walk, git_commit_list_node *commit) +{ + return git_commit_list_insert(commit, &walk->iterator_rand) ? 0 : -1; +} + +static int revwalk_next_timesort(git_commit_list_node **object_out, git_revwalk *walk) +{ + git_commit_list_node *next; + + while ((next = git_pqueue_pop(&walk->iterator_time)) != NULL) { + /* Some commits might become uninteresting after being added to the list */ + if (!next->uninteresting) { + *object_out = next; + return 0; + } + } + + git_error_clear(); + return GIT_ITEROVER; +} + +static int revwalk_next_unsorted(git_commit_list_node **object_out, git_revwalk *walk) +{ + int error; + git_commit_list_node *next; + + while (!(error = get_revision(&next, walk, &walk->iterator_rand))) { + /* Some commits might become uninteresting after being added to the list */ + if (!next->uninteresting) { + *object_out = next; + return 0; + } + } + + return error; +} + +static int revwalk_next_toposort(git_commit_list_node **object_out, git_revwalk *walk) +{ + int error; + git_commit_list_node *next; + + while (!(error = get_revision(&next, walk, &walk->iterator_topo))) { + /* Some commits might become uninteresting after being added to the list */ + if (!next->uninteresting) { + *object_out = next; + return 0; + } + } + + return error; +} + +static int revwalk_next_reverse(git_commit_list_node **object_out, git_revwalk *walk) +{ + *object_out = git_commit_list_pop(&walk->iterator_reverse); + return *object_out ? 0 : GIT_ITEROVER; +} + +static void mark_parents_uninteresting(git_commit_list_node *commit) +{ + unsigned short i; + git_commit_list *parents = NULL; + + for (i = 0; i < commit->out_degree; i++) + git_commit_list_insert(commit->parents[i], &parents); + + + while (parents) { + commit = git_commit_list_pop(&parents); + + while (commit) { + if (commit->uninteresting) + break; + + commit->uninteresting = 1; + /* + * If we've reached this commit some other way + * already, we need to mark its parents uninteresting + * as well. + */ + if (!commit->parents) + break; + + for (i = 0; i < commit->out_degree; i++) + git_commit_list_insert(commit->parents[i], &parents); + commit = commit->parents[0]; + } + } +} + +static int add_parents_to_list(git_revwalk *walk, git_commit_list_node *commit, git_commit_list **list) +{ + unsigned short i; + int error; + + if (commit->added) + return 0; + + commit->added = 1; + + /* + * Go full on in the uninteresting case as we want to include + * as many of these as we can. + * + * Usually we haven't parsed the parent of a parent, but if we + * have it we reached it via other means so we want to mark + * its parents recursively too. + */ + if (commit->uninteresting) { + for (i = 0; i < commit->out_degree; i++) { + git_commit_list_node *p = commit->parents[i]; + p->uninteresting = 1; + + /* git does it gently here, but we don't like missing objects */ + if ((error = git_commit_list_parse(walk, p)) < 0) + return error; + + if (p->parents) + mark_parents_uninteresting(p); + + p->seen = 1; + git_commit_list_insert_by_date(p, list); + } + + return 0; + } + + /* + * Now on to what we do if the commit is indeed + * interesting. Here we do want things like first-parent take + * effect as this is what we'll be showing. + */ + for (i = 0; i < commit->out_degree; i++) { + git_commit_list_node *p = commit->parents[i]; + + if ((error = git_commit_list_parse(walk, p)) < 0) + return error; + + if (walk->hide_cb && walk->hide_cb(&p->oid, walk->hide_cb_payload)) + continue; + + if (!p->seen) { + p->seen = 1; + git_commit_list_insert_by_date(p, list); + } + + if (walk->first_parent) + break; + } + return 0; +} + +/* How many uninteresting commits we want to look at after we run out of interesting ones */ +#define SLOP 5 + +static int still_interesting(git_commit_list *list, int64_t time, int slop) +{ + /* The empty list is pretty boring */ + if (!list) + return 0; + + /* + * If the destination list has commits with an earlier date than our + * source, we want to reset the slop counter as we're not done. + */ + if (time <= list->item->time) + return SLOP; + + for (; list; list = list->next) { + /* + * If the destination list still contains interesting commits we + * want to continue looking. + */ + if (!list->item->uninteresting || list->item->time > time) + return SLOP; + } + + /* Everything's uninteresting, reduce the count */ + return slop - 1; +} + +static int limit_list(git_commit_list **out, git_revwalk *walk, git_commit_list *commits) +{ + int error, slop = SLOP; + int64_t time = INT64_MAX; + git_commit_list *list = commits; + git_commit_list *newlist = NULL; + git_commit_list **p = &newlist; + + while (list) { + git_commit_list_node *commit = git_commit_list_pop(&list); + + if ((error = add_parents_to_list(walk, commit, &list)) < 0) + return error; + + if (commit->uninteresting) { + mark_parents_uninteresting(commit); + + slop = still_interesting(list, time, slop); + if (slop) + continue; + + break; + } + + if (walk->hide_cb && walk->hide_cb(&commit->oid, walk->hide_cb_payload)) + continue; + + time = commit->time; + p = &git_commit_list_insert(commit, p)->next; + } + + git_commit_list_free(&list); + *out = newlist; + return 0; +} + +static int get_revision(git_commit_list_node **out, git_revwalk *walk, git_commit_list **list) +{ + int error; + git_commit_list_node *commit; + + commit = git_commit_list_pop(list); + if (!commit) { + git_error_clear(); + return GIT_ITEROVER; + } + + /* + * If we did not run limit_list and we must add parents to the + * list ourselves. + */ + if (!walk->limited) { + if ((error = add_parents_to_list(walk, commit, list)) < 0) + return error; + } + + *out = commit; + return 0; +} + +static int sort_in_topological_order(git_commit_list **out, git_revwalk *walk, git_commit_list *list) +{ + git_commit_list *ll = NULL, *newlist, **pptr; + git_commit_list_node *next; + git_pqueue queue; + git_vector_cmp queue_cmp = NULL; + unsigned short i; + int error; + + if (walk->sorting & GIT_SORT_TIME) + queue_cmp = git_commit_list_time_cmp; + + if ((error = git_pqueue_init(&queue, 0, 8, queue_cmp))) + return error; + + /* + * Start by resetting the in-degree to 1 for the commits in + * our list. We want to go through this list again, so we + * store it in the commit list as we extract it from the lower + * machinery. + */ + for (ll = list; ll; ll = ll->next) { + ll->item->in_degree = 1; + } + + /* + * Count up how many children each commit has. We limit + * ourselves to those commits in the original list (in-degree + * of 1) avoiding setting it for any parent that was hidden. + */ + for(ll = list; ll; ll = ll->next) { + for (i = 0; i < ll->item->out_degree; ++i) { + git_commit_list_node *parent = ll->item->parents[i]; + if (parent->in_degree) + parent->in_degree++; + } + } + + /* + * Now we find the tips i.e. those not reachable from any other node + * i.e. those which still have an in-degree of 1. + */ + for(ll = list; ll; ll = ll->next) { + if (ll->item->in_degree == 1) { + if ((error = git_pqueue_insert(&queue, ll->item))) + goto cleanup; + } + } + + /* + * We need to output the tips in the order that they came out of the + * traversal, so if we're not doing time-sorting, we need to reverse the + * pqueue in order to get them to come out as we inserted them. + */ + if ((walk->sorting & GIT_SORT_TIME) == 0) + git_pqueue_reverse(&queue); + + + pptr = &newlist; + newlist = NULL; + while ((next = git_pqueue_pop(&queue)) != NULL) { + for (i = 0; i < next->out_degree; ++i) { + git_commit_list_node *parent = next->parents[i]; + if (parent->in_degree == 0) + continue; + + if (--parent->in_degree == 1) { + if ((error = git_pqueue_insert(&queue, parent))) + goto cleanup; + } + } + + /* All the children of 'item' have been emitted (since we got to it via the priority queue) */ + next->in_degree = 0; + + pptr = &git_commit_list_insert(next, pptr)->next; + } + + *out = newlist; + error = 0; + +cleanup: + git_pqueue_free(&queue); + return error; +} + +static int prepare_walk(git_revwalk *walk) +{ + int error = 0; + git_commit_list *list, *commits = NULL, *commits_last = NULL; + git_commit_list_node *next; + + /* If there were no pushes, we know that the walk is already over */ + if (!walk->did_push) { + git_error_clear(); + return GIT_ITEROVER; + } + + /* + * This is a bit convoluted, but necessary to maintain the order of + * the commits. This is especially important in situations where + * git_revwalk__push_glob is called with a git_revwalk__push_options + * setting insert_by_date = 1, which is critical for fetch negotiation. + */ + for (list = walk->user_input; list; list = list->next) { + git_commit_list_node *commit = list->item; + if ((error = git_commit_list_parse(walk, commit)) < 0) + return error; + + if (commit->uninteresting) + mark_parents_uninteresting(commit); + + if (!commit->seen) { + git_commit_list *new_list = NULL; + if ((new_list = git_commit_list_create(commit, NULL)) == NULL) { + git_error_set_oom(); + return -1; + } + + commit->seen = 1; + if (commits_last == NULL) + commits = new_list; + else + commits_last->next = new_list; + + commits_last = new_list; + } + } + + if (walk->limited && (error = limit_list(&commits, walk, commits)) < 0) + return error; + + if (walk->sorting & GIT_SORT_TOPOLOGICAL) { + error = sort_in_topological_order(&walk->iterator_topo, walk, commits); + git_commit_list_free(&commits); + + if (error < 0) + return error; + + walk->get_next = &revwalk_next_toposort; + } else if (walk->sorting & GIT_SORT_TIME) { + for (list = commits; list && !error; list = list->next) + error = walk->enqueue(walk, list->item); + + git_commit_list_free(&commits); + + if (error < 0) + return error; + } else { + walk->iterator_rand = commits; + walk->get_next = revwalk_next_unsorted; + } + + if (walk->sorting & GIT_SORT_REVERSE) { + + while ((error = walk->get_next(&next, walk)) == 0) + if (git_commit_list_insert(next, &walk->iterator_reverse) == NULL) + return -1; + + if (error != GIT_ITEROVER) + return error; + + walk->get_next = &revwalk_next_reverse; + } + + walk->walking = 1; + return 0; +} + + +int git_revwalk_new(git_revwalk **revwalk_out, git_repository *repo) +{ + git_revwalk *walk = git__calloc(1, sizeof(git_revwalk)); + GIT_ERROR_CHECK_ALLOC(walk); + + if (git_oidmap_new(&walk->commits) < 0 || + git_pqueue_init(&walk->iterator_time, 0, 8, git_commit_list_time_cmp) < 0 || + git_pool_init(&walk->commit_pool, COMMIT_ALLOC) < 0) + return -1; + + walk->get_next = &revwalk_next_unsorted; + walk->enqueue = &revwalk_enqueue_unsorted; + + walk->repo = repo; + + if (git_repository_odb(&walk->odb, repo) < 0) { + git_revwalk_free(walk); + return -1; + } + + *revwalk_out = walk; + return 0; +} + +void git_revwalk_free(git_revwalk *walk) +{ + if (walk == NULL) + return; + + git_revwalk_reset(walk); + git_odb_free(walk->odb); + + git_oidmap_free(walk->commits); + git_pool_clear(&walk->commit_pool); + git_pqueue_free(&walk->iterator_time); + git__free(walk); +} + +git_repository *git_revwalk_repository(git_revwalk *walk) +{ + GIT_ASSERT_ARG_WITH_RETVAL(walk, NULL); + + return walk->repo; +} + +int git_revwalk_sorting(git_revwalk *walk, unsigned int sort_mode) +{ + GIT_ASSERT_ARG(walk); + + if (walk->walking) + git_revwalk_reset(walk); + + walk->sorting = sort_mode; + + if (walk->sorting & GIT_SORT_TIME) { + walk->get_next = &revwalk_next_timesort; + walk->enqueue = &revwalk_enqueue_timesort; + } else { + walk->get_next = &revwalk_next_unsorted; + walk->enqueue = &revwalk_enqueue_unsorted; + } + + if (walk->sorting != GIT_SORT_NONE) + walk->limited = 1; + + return 0; +} + +int git_revwalk_simplify_first_parent(git_revwalk *walk) +{ + walk->first_parent = 1; + return 0; +} + +int git_revwalk_next(git_oid *oid, git_revwalk *walk) +{ + int error; + git_commit_list_node *next; + + GIT_ASSERT_ARG(walk); + GIT_ASSERT_ARG(oid); + + if (!walk->walking) { + if ((error = prepare_walk(walk)) < 0) + return error; + } + + error = walk->get_next(&next, walk); + + if (error == GIT_ITEROVER) { + git_revwalk_reset(walk); + git_error_clear(); + return GIT_ITEROVER; + } + + if (!error) + git_oid_cpy(oid, &next->oid); + + return error; +} + +int git_revwalk_reset(git_revwalk *walk) +{ + git_commit_list_node *commit; + + GIT_ASSERT_ARG(walk); + + git_oidmap_foreach_value(walk->commits, commit, { + commit->seen = 0; + commit->in_degree = 0; + commit->topo_delay = 0; + commit->uninteresting = 0; + commit->added = 0; + commit->flags = 0; + }); + + git_pqueue_clear(&walk->iterator_time); + git_commit_list_free(&walk->iterator_topo); + git_commit_list_free(&walk->iterator_rand); + git_commit_list_free(&walk->iterator_reverse); + git_commit_list_free(&walk->user_input); + walk->first_parent = 0; + walk->walking = 0; + walk->limited = 0; + walk->did_push = walk->did_hide = 0; + walk->sorting = GIT_SORT_NONE; + + return 0; +} + +int git_revwalk_add_hide_cb( + git_revwalk *walk, + git_revwalk_hide_cb hide_cb, + void *payload) +{ + GIT_ASSERT_ARG(walk); + + if (walk->walking) + git_revwalk_reset(walk); + + walk->hide_cb = hide_cb; + walk->hide_cb_payload = payload; + + if (hide_cb) + walk->limited = 1; + + return 0; +} + diff --git a/src/libgit2/revwalk.h b/src/libgit2/revwalk.h new file mode 100644 index 0000000..94b8a6f --- /dev/null +++ b/src/libgit2/revwalk.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_revwalk_h__ +#define INCLUDE_revwalk_h__ + +#include "common.h" + +#include "git2/revwalk.h" +#include "oidmap.h" +#include "commit_list.h" +#include "pqueue.h" +#include "pool.h" +#include "vector.h" + +#include "oidmap.h" + +struct git_revwalk { + git_repository *repo; + git_odb *odb; + + git_oidmap *commits; + git_pool commit_pool; + + git_commit_list *iterator_topo; + git_commit_list *iterator_rand; + git_commit_list *iterator_reverse; + git_pqueue iterator_time; + + int (*get_next)(git_commit_list_node **, git_revwalk *); + int (*enqueue)(git_revwalk *, git_commit_list_node *); + + unsigned walking:1, + first_parent: 1, + did_hide: 1, + did_push: 1, + limited: 1; + unsigned int sorting; + + /* the pushes and hides */ + git_commit_list *user_input; + + /* hide callback */ + git_revwalk_hide_cb hide_cb; + void *hide_cb_payload; +}; + +git_commit_list_node *git_revwalk__commit_lookup(git_revwalk *walk, const git_oid *oid); + +typedef struct { + int uninteresting; + int from_glob; + int insert_by_date; +} git_revwalk__push_options; + +#define GIT_REVWALK__PUSH_OPTIONS_INIT { 0 } + +int git_revwalk__push_commit(git_revwalk *walk, + const git_oid *oid, + const git_revwalk__push_options *opts); + +int git_revwalk__push_ref(git_revwalk *walk, + const char *refname, + const git_revwalk__push_options *opts); + +int git_revwalk__push_glob(git_revwalk *walk, + const char *glob, + const git_revwalk__push_options *given_opts); + +#endif diff --git a/src/libgit2/settings.h b/src/libgit2/settings.h new file mode 100644 index 0000000..dc42ce9 --- /dev/null +++ b/src/libgit2/settings.h @@ -0,0 +1,11 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +extern int git_settings_global_init(void); + +extern const char *git_libgit2__user_agent(void); +extern const char *git_libgit2__ssl_ciphers(void); diff --git a/src/libgit2/signature.c b/src/libgit2/signature.c new file mode 100644 index 0000000..5d6ab57 --- /dev/null +++ b/src/libgit2/signature.c @@ -0,0 +1,339 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "signature.h" + +#include "repository.h" +#include "git2/common.h" +#include "posix.h" + +void git_signature_free(git_signature *sig) +{ + if (sig == NULL) + return; + + git__free(sig->name); + sig->name = NULL; + git__free(sig->email); + sig->email = NULL; + git__free(sig); +} + +static int signature_parse_error(const char *msg) +{ + git_error_set(GIT_ERROR_INVALID, "failed to parse signature - %s", msg); + return GIT_EINVALID; +} + +static int signature_error(const char *msg) +{ + git_error_set(GIT_ERROR_INVALID, "failed to parse signature - %s", msg); + return -1; +} + +static bool contains_angle_brackets(const char *input) +{ + return strchr(input, '<') != NULL || strchr(input, '>') != NULL; +} + +static bool is_crud(unsigned char c) +{ + return c <= 32 || + c == '.' || + c == ',' || + c == ':' || + c == ';' || + c == '<' || + c == '>' || + c == '"' || + c == '\\' || + c == '\''; +} + +static char *extract_trimmed(const char *ptr, size_t len) +{ + while (len && is_crud((unsigned char)ptr[0])) { + ptr++; len--; + } + + while (len && is_crud((unsigned char)ptr[len - 1])) { + len--; + } + + return git__substrdup(ptr, len); +} + +int git_signature_new(git_signature **sig_out, const char *name, const char *email, git_time_t time, int offset) +{ + git_signature *p = NULL; + + GIT_ASSERT_ARG(name); + GIT_ASSERT_ARG(email); + + *sig_out = NULL; + + if (contains_angle_brackets(name) || + contains_angle_brackets(email)) { + return signature_error( + "Neither `name` nor `email` should contain angle brackets chars."); + } + + p = git__calloc(1, sizeof(git_signature)); + GIT_ERROR_CHECK_ALLOC(p); + + p->name = extract_trimmed(name, strlen(name)); + GIT_ERROR_CHECK_ALLOC(p->name); + p->email = extract_trimmed(email, strlen(email)); + GIT_ERROR_CHECK_ALLOC(p->email); + + if (p->name[0] == '\0' || p->email[0] == '\0') { + git_signature_free(p); + return signature_error("Signature cannot have an empty name or email"); + } + + p->when.time = time; + p->when.offset = offset; + p->when.sign = (offset < 0) ? '-' : '+'; + + *sig_out = p; + return 0; +} + +int git_signature_dup(git_signature **dest, const git_signature *source) +{ + git_signature *signature; + + if (source == NULL) + return 0; + + signature = git__calloc(1, sizeof(git_signature)); + GIT_ERROR_CHECK_ALLOC(signature); + + signature->name = git__strdup(source->name); + GIT_ERROR_CHECK_ALLOC(signature->name); + + signature->email = git__strdup(source->email); + GIT_ERROR_CHECK_ALLOC(signature->email); + + signature->when.time = source->when.time; + signature->when.offset = source->when.offset; + signature->when.sign = source->when.sign; + + *dest = signature; + + return 0; +} + +int git_signature__pdup(git_signature **dest, const git_signature *source, git_pool *pool) +{ + git_signature *signature; + + if (source == NULL) + return 0; + + signature = git_pool_mallocz(pool, sizeof(git_signature)); + GIT_ERROR_CHECK_ALLOC(signature); + + signature->name = git_pool_strdup(pool, source->name); + GIT_ERROR_CHECK_ALLOC(signature->name); + + signature->email = git_pool_strdup(pool, source->email); + GIT_ERROR_CHECK_ALLOC(signature->email); + + signature->when.time = source->when.time; + signature->when.offset = source->when.offset; + signature->when.sign = source->when.sign; + + *dest = signature; + + return 0; +} + +int git_signature_now(git_signature **sig_out, const char *name, const char *email) +{ + time_t now; + time_t offset; + struct tm *utc_tm; + git_signature *sig; + struct tm _utc; + + *sig_out = NULL; + + /* + * Get the current time as seconds since the epoch and + * transform that into a tm struct containing the time at + * UTC. Give that to mktime which considers it a local time + * (tm_isdst = -1 asks it to take DST into account) and gives + * us that time as seconds since the epoch. The difference + * between its return value and 'now' is our offset to UTC. + */ + time(&now); + utc_tm = p_gmtime_r(&now, &_utc); + utc_tm->tm_isdst = -1; + offset = (time_t)difftime(now, mktime(utc_tm)); + offset /= 60; + + if (git_signature_new(&sig, name, email, now, (int)offset) < 0) + return -1; + + *sig_out = sig; + + return 0; +} + +int git_signature_default(git_signature **out, git_repository *repo) +{ + int error; + git_config *cfg; + const char *user_name, *user_email; + + if ((error = git_repository_config_snapshot(&cfg, repo)) < 0) + return error; + + if (!(error = git_config_get_string(&user_name, cfg, "user.name")) && + !(error = git_config_get_string(&user_email, cfg, "user.email"))) + error = git_signature_now(out, user_name, user_email); + + git_config_free(cfg); + return error; +} + +int git_signature__parse(git_signature *sig, const char **buffer_out, + const char *buffer_end, const char *header, char ender) +{ + const char *buffer = *buffer_out; + const char *email_start, *email_end; + + memset(sig, 0, sizeof(git_signature)); + + if (ender && + (buffer_end = memchr(buffer, ender, buffer_end - buffer)) == NULL) + return signature_parse_error("no newline given"); + + if (header) { + const size_t header_len = strlen(header); + + if (buffer + header_len >= buffer_end || memcmp(buffer, header, header_len) != 0) + return signature_parse_error("expected prefix doesn't match actual"); + + buffer += header_len; + } + + email_start = git__memrchr(buffer, '<', buffer_end - buffer); + email_end = git__memrchr(buffer, '>', buffer_end - buffer); + + if (!email_start || !email_end || email_end <= email_start) + return signature_parse_error("malformed e-mail"); + + email_start += 1; + sig->name = extract_trimmed(buffer, email_start - buffer - 1); + sig->email = extract_trimmed(email_start, email_end - email_start); + + /* Do we even have a time at the end of the signature? */ + if (email_end + 2 < buffer_end) { + const char *time_start = email_end + 2; + const char *time_end; + + if (git__strntol64(&sig->when.time, time_start, + buffer_end - time_start, &time_end, 10) < 0) { + git__free(sig->name); + git__free(sig->email); + sig->name = sig->email = NULL; + return signature_parse_error("invalid Unix timestamp"); + } + + /* do we have a timezone? */ + if (time_end + 1 < buffer_end) { + int offset, hours, mins; + const char *tz_start, *tz_end; + + tz_start = time_end + 1; + + if ((tz_start[0] != '-' && tz_start[0] != '+') || + git__strntol32(&offset, tz_start + 1, + buffer_end - tz_start - 1, &tz_end, 10) < 0) { + /* malformed timezone, just assume it's zero */ + offset = 0; + } + + hours = offset / 100; + mins = offset % 100; + + /* + * only store timezone if it's not overflowing; + * see http://www.worldtimezone.com/faq.html + */ + if (hours <= 14 && mins <= 59) { + sig->when.offset = (hours * 60) + mins; + sig->when.sign = tz_start[0]; + if (tz_start[0] == '-') + sig->when.offset = -sig->when.offset; + } + } + } + + *buffer_out = buffer_end + 1; + return 0; +} + +int git_signature_from_buffer(git_signature **out, const char *buf) +{ + git_signature *sig; + const char *buf_end; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(buf); + + *out = NULL; + + sig = git__calloc(1, sizeof(git_signature)); + GIT_ERROR_CHECK_ALLOC(sig); + + buf_end = buf + strlen(buf); + error = git_signature__parse(sig, &buf, buf_end, NULL, '\0'); + + if (error) + git__free(sig); + else + *out = sig; + + return error; +} + +void git_signature__writebuf(git_str *buf, const char *header, const git_signature *sig) +{ + int offset, hours, mins; + char sign; + + offset = sig->when.offset; + sign = (sig->when.offset < 0 || sig->when.sign == '-') ? '-' : '+'; + + if (offset < 0) + offset = -offset; + + hours = offset / 60; + mins = offset % 60; + + git_str_printf(buf, "%s%s <%s> %u %c%02d%02d\n", + header ? header : "", sig->name, sig->email, + (unsigned)sig->when.time, sign, hours, mins); +} + +bool git_signature__equal(const git_signature *one, const git_signature *two) +{ + GIT_ASSERT_ARG(one); + GIT_ASSERT_ARG(two); + + return + git__strcmp(one->name, two->name) == 0 && + git__strcmp(one->email, two->email) == 0 && + one->when.time == two->when.time && + one->when.offset == two->when.offset && + one->when.sign == two->when.sign; +} + diff --git a/src/libgit2/signature.h b/src/libgit2/signature.h new file mode 100644 index 0000000..5c82709 --- /dev/null +++ b/src/libgit2/signature.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_signature_h__ +#define INCLUDE_signature_h__ + +#include "common.h" + +#include "git2/common.h" +#include "git2/signature.h" +#include "repository.h" +#include + +int git_signature__parse(git_signature *sig, const char **buffer_out, const char *buffer_end, const char *header, char ender); +void git_signature__writebuf(git_str *buf, const char *header, const git_signature *sig); +bool git_signature__equal(const git_signature *one, const git_signature *two); + +int git_signature__pdup(git_signature **dest, const git_signature *source, git_pool *pool); + +#endif diff --git a/src/libgit2/stash.c b/src/libgit2/stash.c new file mode 100644 index 0000000..b49e95c --- /dev/null +++ b/src/libgit2/stash.c @@ -0,0 +1,1286 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "repository.h" +#include "commit.h" +#include "tree.h" +#include "reflog.h" +#include "blob.h" +#include "git2/diff.h" +#include "git2/stash.h" +#include "git2/status.h" +#include "git2/checkout.h" +#include "git2/index.h" +#include "git2/transaction.h" +#include "git2/merge.h" +#include "index.h" +#include "signature.h" +#include "iterator.h" +#include "merge.h" +#include "diff.h" +#include "diff_generate.h" +#include "strarray.h" + +static int create_error(int error, const char *msg) +{ + git_error_set(GIT_ERROR_STASH, "cannot stash changes - %s", msg); + return error; +} + +static int retrieve_head(git_reference **out, git_repository *repo) +{ + int error = git_repository_head(out, repo); + + if (error == GIT_EUNBORNBRANCH) + return create_error(error, "you do not have the initial commit yet."); + + return error; +} + +static int append_abbreviated_oid(git_str *out, const git_oid *b_commit) +{ + char *formatted_oid; + + formatted_oid = git_oid_allocfmt(b_commit); + GIT_ERROR_CHECK_ALLOC(formatted_oid); + + git_str_put(out, formatted_oid, 7); + git__free(formatted_oid); + + return git_str_oom(out) ? -1 : 0; +} + +static int append_commit_description(git_str *out, git_commit *commit) +{ + const char *summary = git_commit_summary(commit); + GIT_ERROR_CHECK_ALLOC(summary); + + if (append_abbreviated_oid(out, git_commit_id(commit)) < 0) + return -1; + + git_str_putc(out, ' '); + git_str_puts(out, summary); + git_str_putc(out, '\n'); + + return git_str_oom(out) ? -1 : 0; +} + +static int retrieve_base_commit_and_message( + git_commit **b_commit, + git_str *stash_message, + git_repository *repo) +{ + git_reference *head = NULL; + int error; + + if ((error = retrieve_head(&head, repo)) < 0) + return error; + + if (strcmp("HEAD", git_reference_name(head)) == 0) + error = git_str_puts(stash_message, "(no branch): "); + else + error = git_str_printf( + stash_message, + "%s: ", + git_reference_name(head) + strlen(GIT_REFS_HEADS_DIR)); + if (error < 0) + goto cleanup; + + if ((error = git_commit_lookup( + b_commit, repo, git_reference_target(head))) < 0) + goto cleanup; + + if ((error = append_commit_description(stash_message, *b_commit)) < 0) + goto cleanup; + +cleanup: + git_reference_free(head); + return error; +} + +static int build_tree_from_index( + git_tree **out, + git_repository *repo, + git_index *index) +{ + int error; + git_oid i_tree_oid; + + if ((error = git_index_write_tree_to(&i_tree_oid, index, repo)) < 0) + return error; + + return git_tree_lookup(out, repo, &i_tree_oid); +} + +static int commit_index( + git_commit **i_commit, + git_repository *repo, + git_index *index, + const git_signature *stasher, + const char *message, + const git_commit *parent) +{ + git_tree *i_tree = NULL; + git_oid i_commit_oid; + git_str msg = GIT_STR_INIT; + int error; + + if ((error = build_tree_from_index(&i_tree, repo, index)) < 0) + goto cleanup; + + if ((error = git_str_printf(&msg, "index on %s\n", message)) < 0) + goto cleanup; + + if ((error = git_commit_create( + &i_commit_oid, + git_index_owner(index), + NULL, + stasher, + stasher, + NULL, + git_str_cstr(&msg), + i_tree, + 1, + &parent)) < 0) + goto cleanup; + + error = git_commit_lookup(i_commit, git_index_owner(index), &i_commit_oid); + +cleanup: + git_tree_free(i_tree); + git_str_dispose(&msg); + return error; +} + +struct stash_update_rules { + bool include_changed; + bool include_untracked; + bool include_ignored; +}; + +/* + * Similar to git_index_add_bypath but able to operate on any + * index without making assumptions about the repository's index + */ +static int stash_to_index( + git_repository *repo, + git_index *index, + const char *path) +{ + git_index *repo_index = NULL; + git_index_entry entry = {{0}}; + struct stat st; + int error; + + if (!git_repository_is_bare(repo) && + (error = git_repository_index__weakptr(&repo_index, repo)) < 0) + return error; + + if ((error = git_blob__create_from_paths( + &entry.id, &st, repo, NULL, path, 0, true)) < 0) + return error; + + git_index_entry__init_from_stat(&entry, &st, + (repo_index == NULL || !repo_index->distrust_filemode)); + + entry.path = path; + + return git_index_add(index, &entry); +} + +static int stash_update_index_from_paths( + git_repository *repo, + git_index *index, + const git_strarray *paths) +{ + unsigned int status_flags; + size_t i; + int error = 0; + + for (i = 0; i < paths->count; i++) { + git_status_file(&status_flags, repo, paths->strings[i]); + + if (status_flags & (GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_DELETED)) { + if ((error = git_index_remove(index, paths->strings[i], 0)) < 0) + return error; + } else { + if ((error = stash_to_index(repo, index, paths->strings[i])) < 0) + return error; + } + } + + return error; +} + +static int stash_update_index_from_diff( + git_repository *repo, + git_index *index, + const git_diff *diff, + struct stash_update_rules *data) +{ + int error = 0; + size_t d, max_d = git_diff_num_deltas(diff); + + for (d = 0; !error && d < max_d; ++d) { + const char *add_path = NULL; + const git_diff_delta *delta = git_diff_get_delta(diff, d); + + switch (delta->status) { + case GIT_DELTA_IGNORED: + if (data->include_ignored) + add_path = delta->new_file.path; + break; + + case GIT_DELTA_UNTRACKED: + if (data->include_untracked && + delta->new_file.mode != GIT_FILEMODE_TREE) + add_path = delta->new_file.path; + break; + + case GIT_DELTA_ADDED: + case GIT_DELTA_MODIFIED: + if (data->include_changed) + add_path = delta->new_file.path; + break; + + case GIT_DELTA_DELETED: + if (data->include_changed && + !git_index_find(NULL, index, delta->old_file.path)) + error = git_index_remove(index, delta->old_file.path, 0); + break; + + default: + /* Unimplemented */ + git_error_set( + GIT_ERROR_INVALID, + "cannot update index. Unimplemented status (%d)", + delta->status); + return -1; + } + + if (add_path != NULL) + error = stash_to_index(repo, index, add_path); + } + + return error; +} + +static int build_untracked_tree( + git_tree **tree_out, + git_repository *repo, + git_commit *i_commit, + uint32_t flags) +{ + git_index *i_index = NULL; + git_tree *i_tree = NULL; + git_diff *diff = NULL; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + struct stash_update_rules data = {0}; + int error; + + if ((error = git_index__new(&i_index, repo->oid_type)) < 0) + goto cleanup; + + if (flags & GIT_STASH_INCLUDE_UNTRACKED) { + opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED | + GIT_DIFF_RECURSE_UNTRACKED_DIRS; + data.include_untracked = true; + } + + if (flags & GIT_STASH_INCLUDE_IGNORED) { + opts.flags |= GIT_DIFF_INCLUDE_IGNORED | + GIT_DIFF_RECURSE_IGNORED_DIRS; + data.include_ignored = true; + } + + if ((error = git_commit_tree(&i_tree, i_commit)) < 0) + goto cleanup; + + if ((error = git_diff_tree_to_workdir(&diff, repo, i_tree, &opts)) < 0) + goto cleanup; + + if ((error = stash_update_index_from_diff(repo, i_index, diff, &data)) < 0) + goto cleanup; + + error = build_tree_from_index(tree_out, repo, i_index); + +cleanup: + git_diff_free(diff); + git_tree_free(i_tree); + git_index_free(i_index); + return error; +} + +static int commit_untracked( + git_commit **u_commit, + git_repository *repo, + const git_signature *stasher, + const char *message, + git_commit *i_commit, + uint32_t flags) +{ + git_tree *u_tree = NULL; + git_oid u_commit_oid; + git_str msg = GIT_STR_INIT; + int error; + + if ((error = build_untracked_tree(&u_tree, repo, i_commit, flags)) < 0) + goto cleanup; + + if ((error = git_str_printf(&msg, "untracked files on %s\n", message)) < 0) + goto cleanup; + + if ((error = git_commit_create( + &u_commit_oid, + repo, + NULL, + stasher, + stasher, + NULL, + git_str_cstr(&msg), + u_tree, + 0, + NULL)) < 0) + goto cleanup; + + error = git_commit_lookup(u_commit, repo, &u_commit_oid); + +cleanup: + git_tree_free(u_tree); + git_str_dispose(&msg); + return error; +} + +static git_diff_delta *stash_delta_merge( + const git_diff_delta *a, + const git_diff_delta *b, + git_pool *pool) +{ + /* Special case for stash: if a file is deleted in the index, but exists + * in the working tree, we need to stash the workdir copy for the workdir. + */ + if (a->status == GIT_DELTA_DELETED && b->status == GIT_DELTA_UNTRACKED) { + git_diff_delta *dup = git_diff__delta_dup(b, pool); + + if (dup) + dup->status = GIT_DELTA_MODIFIED; + return dup; + } + + return git_diff__merge_like_cgit(a, b, pool); +} + +static int build_workdir_tree( + git_tree **tree_out, + git_repository *repo, + git_index *i_index, + git_commit *b_commit) +{ + git_tree *b_tree = NULL; + git_diff *diff = NULL, *idx_to_wd = NULL; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + struct stash_update_rules data = {0}; + int error; + + opts.flags = GIT_DIFF_IGNORE_SUBMODULES | GIT_DIFF_INCLUDE_UNTRACKED; + + if ((error = git_commit_tree(&b_tree, b_commit)) < 0) + goto cleanup; + + if ((error = git_diff_tree_to_index(&diff, repo, b_tree, i_index, &opts)) < 0 || + (error = git_diff_index_to_workdir(&idx_to_wd, repo, i_index, &opts)) < 0 || + (error = git_diff__merge(diff, idx_to_wd, stash_delta_merge)) < 0) + goto cleanup; + + data.include_changed = true; + + if ((error = stash_update_index_from_diff(repo, i_index, diff, &data)) < 0) + goto cleanup; + + error = build_tree_from_index(tree_out, repo, i_index); + +cleanup: + git_diff_free(idx_to_wd); + git_diff_free(diff); + git_tree_free(b_tree); + + return error; +} + +static int build_stash_commit_from_tree( + git_oid *w_commit_oid, + git_repository *repo, + const git_signature *stasher, + const char *message, + git_commit *i_commit, + git_commit *b_commit, + git_commit *u_commit, + const git_tree *tree) +{ + const git_commit *parents[] = { NULL, NULL, NULL }; + + parents[0] = b_commit; + parents[1] = i_commit; + parents[2] = u_commit; + + return git_commit_create( + w_commit_oid, + repo, + NULL, + stasher, + stasher, + NULL, + message, + tree, + u_commit ? 3 : 2, + parents); +} + +static int build_stash_commit_from_index( + git_oid *w_commit_oid, + git_repository *repo, + const git_signature *stasher, + const char *message, + git_commit *i_commit, + git_commit *b_commit, + git_commit *u_commit, + git_index *index) +{ + git_tree *tree; + int error; + + if ((error = build_tree_from_index(&tree, repo, index)) < 0) + goto cleanup; + + error = build_stash_commit_from_tree( + w_commit_oid, + repo, + stasher, + message, + i_commit, + b_commit, + u_commit, + tree); + +cleanup: + git_tree_free(tree); + return error; +} + +static int commit_worktree( + git_oid *w_commit_oid, + git_repository *repo, + const git_signature *stasher, + const char *message, + git_commit *i_commit, + git_commit *b_commit, + git_commit *u_commit) +{ + git_index *i_index = NULL, *r_index = NULL; + git_tree *w_tree = NULL; + int error = 0, ignorecase; + + if ((error = git_repository_index(&r_index, repo) < 0) || + (error = git_index__new(&i_index, repo->oid_type)) < 0 || + (error = git_index__fill(i_index, &r_index->entries) < 0) || + (error = git_repository__configmap_lookup(&ignorecase, repo, GIT_CONFIGMAP_IGNORECASE)) < 0) + goto cleanup; + + git_index__set_ignore_case(i_index, ignorecase); + + if ((error = build_workdir_tree(&w_tree, repo, i_index, b_commit)) < 0) + goto cleanup; + + error = build_stash_commit_from_tree( + w_commit_oid, + repo, + stasher, + message, + i_commit, + b_commit, + u_commit, + w_tree + ); + +cleanup: + git_tree_free(w_tree); + git_index_free(i_index); + git_index_free(r_index); + return error; +} + +static int prepare_worktree_commit_message(git_str *out, const char *user_message) +{ + git_str buf = GIT_STR_INIT; + int error = 0; + + if (!user_message) { + git_str_printf(&buf, "WIP on %s", git_str_cstr(out)); + } else { + const char *colon; + + if ((colon = strchr(git_str_cstr(out), ':')) == NULL) + goto cleanup; + + git_str_puts(&buf, "On "); + git_str_put(&buf, git_str_cstr(out), colon - out->ptr); + git_str_printf(&buf, ": %s\n", user_message); + } + + if (git_str_oom(&buf)) { + error = -1; + goto cleanup; + } + + git_str_swap(out, &buf); + +cleanup: + git_str_dispose(&buf); + return error; +} + +static int update_reflog( + git_oid *w_commit_oid, + git_repository *repo, + const char *message) +{ + git_reference *stash; + int error; + + if ((error = git_reference_ensure_log(repo, GIT_REFS_STASH_FILE)) < 0) + return error; + + error = git_reference_create(&stash, repo, GIT_REFS_STASH_FILE, w_commit_oid, 1, message); + + git_reference_free(stash); + + return error; +} + +static int is_dirty_cb(const char *path, unsigned int status, void *payload) +{ + GIT_UNUSED(path); + GIT_UNUSED(status); + GIT_UNUSED(payload); + + return GIT_PASSTHROUGH; +} + +static int ensure_there_are_changes_to_stash(git_repository *repo, uint32_t flags) +{ + int error; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + opts.flags = GIT_STATUS_OPT_EXCLUDE_SUBMODULES; + + if (flags & GIT_STASH_INCLUDE_UNTRACKED) + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + if (flags & GIT_STASH_INCLUDE_IGNORED) + opts.flags |= GIT_STATUS_OPT_INCLUDE_IGNORED | + GIT_STATUS_OPT_RECURSE_IGNORED_DIRS; + + error = git_status_foreach_ext(repo, &opts, is_dirty_cb, NULL); + + if (error == GIT_PASSTHROUGH) + return 0; + + if (!error) + return create_error(GIT_ENOTFOUND, "there is nothing to stash."); + + return error; +} + +static int has_changes_cb( + const char *path, + unsigned int status, + void *payload) +{ + GIT_UNUSED(path); + GIT_UNUSED(status); + GIT_UNUSED(payload); + + if (status == GIT_STATUS_CURRENT) + return GIT_ENOTFOUND; + + return 0; +} + +static int ensure_there_are_changes_to_stash_paths( + git_repository *repo, + uint32_t flags, + const git_strarray *paths) +{ + int error; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + opts.flags = GIT_STATUS_OPT_EXCLUDE_SUBMODULES | + GIT_STATUS_OPT_INCLUDE_UNMODIFIED | + GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH; + + if (flags & GIT_STASH_INCLUDE_UNTRACKED) + opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + if (flags & GIT_STASH_INCLUDE_IGNORED) + opts.flags |= GIT_STATUS_OPT_INCLUDE_IGNORED | + GIT_STATUS_OPT_RECURSE_IGNORED_DIRS; + + git_strarray_copy(&opts.pathspec, paths); + + error = git_status_foreach_ext(repo, &opts, has_changes_cb, NULL); + + git_strarray_dispose(&opts.pathspec); + + if (error == GIT_ENOTFOUND) + return create_error(GIT_ENOTFOUND, "one of the files does not have any changes to stash."); + + return error; +} + +static int reset_index_and_workdir(git_repository *repo, git_commit *commit, uint32_t flags) +{ + git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; + + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + if (flags & GIT_STASH_INCLUDE_UNTRACKED) + opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED; + if (flags & GIT_STASH_INCLUDE_IGNORED) + opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_IGNORED; + + return git_checkout_tree(repo, (git_object *)commit, &opts); +} + +int git_stash_save( + git_oid *out, + git_repository *repo, + const git_signature *stasher, + const char *message, + uint32_t flags) +{ + git_stash_save_options opts = GIT_STASH_SAVE_OPTIONS_INIT; + + GIT_ASSERT_ARG(stasher); + + opts.stasher = stasher; + opts.message = message; + opts.flags = flags; + + return git_stash_save_with_opts(out, repo, &opts); +} + +int git_stash_save_with_opts( + git_oid *out, + git_repository *repo, + const git_stash_save_options *opts) +{ + git_index *index = NULL, *paths_index = NULL; + git_commit *b_commit = NULL, *i_commit = NULL, *u_commit = NULL; + git_str msg = GIT_STR_INIT; + git_tree *tree = NULL; + git_reference *head = NULL; + bool has_paths = false; + + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(opts && opts->stasher); + + has_paths = opts->paths.count > 0; + + if ((error = git_repository__ensure_not_bare(repo, "stash save")) < 0) + return error; + + if ((error = retrieve_base_commit_and_message(&b_commit, &msg, repo)) < 0) + goto cleanup; + + if (!has_paths && + (error = ensure_there_are_changes_to_stash(repo, opts->flags)) < 0) + goto cleanup; + else if (has_paths && + (error = ensure_there_are_changes_to_stash_paths( + repo, opts->flags, &opts->paths)) < 0) + goto cleanup; + + if ((error = git_repository_index(&index, repo)) < 0) + goto cleanup; + + if ((error = commit_index(&i_commit, repo, index, opts->stasher, + git_str_cstr(&msg), b_commit)) < 0) + goto cleanup; + + if ((opts->flags & (GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)) && + (error = commit_untracked(&u_commit, repo, opts->stasher, + git_str_cstr(&msg), i_commit, opts->flags)) < 0) + goto cleanup; + + if ((error = prepare_worktree_commit_message(&msg, opts->message)) < 0) + goto cleanup; + + if (!has_paths) { + if ((error = commit_worktree(out, repo, opts->stasher, git_str_cstr(&msg), + i_commit, b_commit, u_commit)) < 0) + goto cleanup; + } else { + if ((error = git_index__new(&paths_index, repo->oid_type)) < 0 || + (error = retrieve_head(&head, repo)) < 0 || + (error = git_reference_peel((git_object**)&tree, head, GIT_OBJECT_TREE)) < 0 || + (error = git_index_read_tree(paths_index, tree)) < 0 || + (error = stash_update_index_from_paths(repo, paths_index, &opts->paths)) < 0 || + (error = build_stash_commit_from_index(out, repo, opts->stasher, git_str_cstr(&msg), + i_commit, b_commit, u_commit, paths_index)) < 0) + goto cleanup; + } + + git_str_rtrim(&msg); + + if ((error = update_reflog(out, repo, git_str_cstr(&msg))) < 0) + goto cleanup; + + if (!(opts->flags & GIT_STASH_KEEP_ALL) && + (error = reset_index_and_workdir(repo, + (opts->flags & GIT_STASH_KEEP_INDEX) ? i_commit : b_commit,opts->flags)) < 0) + goto cleanup; + +cleanup: + git_str_dispose(&msg); + git_commit_free(i_commit); + git_commit_free(b_commit); + git_commit_free(u_commit); + git_tree_free(tree); + git_reference_free(head); + git_index_free(index); + git_index_free(paths_index); + + return error; +} + +static int retrieve_stash_commit( + git_commit **commit, + git_repository *repo, + size_t index) +{ + git_reference *stash = NULL; + git_reflog *reflog = NULL; + int error; + size_t max; + const git_reflog_entry *entry; + + if ((error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE)) < 0) + goto cleanup; + + if ((error = git_reflog_read(&reflog, repo, GIT_REFS_STASH_FILE)) < 0) + goto cleanup; + + max = git_reflog_entrycount(reflog); + if (!max || index > max - 1) { + error = GIT_ENOTFOUND; + git_error_set(GIT_ERROR_STASH, "no stashed state at position %" PRIuZ, index); + goto cleanup; + } + + entry = git_reflog_entry_byindex(reflog, index); + if ((error = git_commit_lookup(commit, repo, git_reflog_entry_id_new(entry))) < 0) + goto cleanup; + +cleanup: + git_reference_free(stash); + git_reflog_free(reflog); + return error; +} + +static int retrieve_stash_trees( + git_tree **out_stash_tree, + git_tree **out_base_tree, + git_tree **out_index_tree, + git_tree **out_index_parent_tree, + git_tree **out_untracked_tree, + git_commit *stash_commit) +{ + git_tree *stash_tree = NULL; + git_commit *base_commit = NULL; + git_tree *base_tree = NULL; + git_commit *index_commit = NULL; + git_tree *index_tree = NULL; + git_commit *index_parent_commit = NULL; + git_tree *index_parent_tree = NULL; + git_commit *untracked_commit = NULL; + git_tree *untracked_tree = NULL; + int error; + + if ((error = git_commit_tree(&stash_tree, stash_commit)) < 0) + goto cleanup; + + if ((error = git_commit_parent(&base_commit, stash_commit, 0)) < 0) + goto cleanup; + if ((error = git_commit_tree(&base_tree, base_commit)) < 0) + goto cleanup; + + if ((error = git_commit_parent(&index_commit, stash_commit, 1)) < 0) + goto cleanup; + if ((error = git_commit_tree(&index_tree, index_commit)) < 0) + goto cleanup; + + if ((error = git_commit_parent(&index_parent_commit, index_commit, 0)) < 0) + goto cleanup; + if ((error = git_commit_tree(&index_parent_tree, index_parent_commit)) < 0) + goto cleanup; + + if (git_commit_parentcount(stash_commit) == 3) { + if ((error = git_commit_parent(&untracked_commit, stash_commit, 2)) < 0) + goto cleanup; + if ((error = git_commit_tree(&untracked_tree, untracked_commit)) < 0) + goto cleanup; + } + + *out_stash_tree = stash_tree; + *out_base_tree = base_tree; + *out_index_tree = index_tree; + *out_index_parent_tree = index_parent_tree; + *out_untracked_tree = untracked_tree; + +cleanup: + git_commit_free(untracked_commit); + git_commit_free(index_parent_commit); + git_commit_free(index_commit); + git_commit_free(base_commit); + if (error < 0) { + git_tree_free(stash_tree); + git_tree_free(base_tree); + git_tree_free(index_tree); + git_tree_free(index_parent_tree); + git_tree_free(untracked_tree); + } + return error; +} + +static int merge_indexes( + git_index **out, + git_repository *repo, + git_tree *ancestor_tree, + git_index *ours_index, + git_index *theirs_index) +{ + git_iterator *ancestor = NULL, *ours = NULL, *theirs = NULL; + git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT; + int error; + + iter_opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE; + + if ((error = git_iterator_for_tree(&ancestor, ancestor_tree, &iter_opts)) < 0 || + (error = git_iterator_for_index(&ours, repo, ours_index, &iter_opts)) < 0 || + (error = git_iterator_for_index(&theirs, repo, theirs_index, &iter_opts)) < 0) + goto done; + + error = git_merge__iterators(out, repo, ancestor, ours, theirs, NULL); + +done: + git_iterator_free(ancestor); + git_iterator_free(ours); + git_iterator_free(theirs); + return error; +} + +static int merge_index_and_tree( + git_index **out, + git_repository *repo, + git_tree *ancestor_tree, + git_index *ours_index, + git_tree *theirs_tree) +{ + git_iterator *ancestor = NULL, *ours = NULL, *theirs = NULL; + git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT; + int error; + + iter_opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE; + + if ((error = git_iterator_for_tree(&ancestor, ancestor_tree, &iter_opts)) < 0 || + (error = git_iterator_for_index(&ours, repo, ours_index, &iter_opts)) < 0 || + (error = git_iterator_for_tree(&theirs, theirs_tree, &iter_opts)) < 0) + goto done; + + error = git_merge__iterators(out, repo, ancestor, ours, theirs, NULL); + +done: + git_iterator_free(ancestor); + git_iterator_free(ours); + git_iterator_free(theirs); + return error; +} + +static void normalize_apply_options( + git_stash_apply_options *opts, + const git_stash_apply_options *given_apply_opts) +{ + if (given_apply_opts != NULL) { + memcpy(opts, given_apply_opts, sizeof(git_stash_apply_options)); + } else { + git_stash_apply_options default_apply_opts = GIT_STASH_APPLY_OPTIONS_INIT; + memcpy(opts, &default_apply_opts, sizeof(git_stash_apply_options)); + } + + opts->checkout_options.checkout_strategy |= GIT_CHECKOUT_NO_REFRESH; + + if (!opts->checkout_options.our_label) + opts->checkout_options.our_label = "Updated upstream"; + + if (!opts->checkout_options.their_label) + opts->checkout_options.their_label = "Stashed changes"; +} + +int git_stash_apply_options_init(git_stash_apply_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_stash_apply_options, GIT_STASH_APPLY_OPTIONS_INIT); + return 0; +} + +int git_stash_save_options_init(git_stash_save_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_stash_save_options, GIT_STASH_SAVE_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_stash_apply_init_options(git_stash_apply_options *opts, unsigned int version) +{ + return git_stash_apply_options_init(opts, version); +} +#endif + +#define NOTIFY_PROGRESS(opts, progress_type) \ + do { \ + if ((opts).progress_cb && \ + (error = (opts).progress_cb((progress_type), (opts).progress_payload))) { \ + error = (error < 0) ? error : -1; \ + goto cleanup; \ + } \ + } while(false); + +static int ensure_clean_index(git_repository *repo, git_index *index) +{ + git_tree *head_tree = NULL; + git_diff *index_diff = NULL; + int error = 0; + + if ((error = git_repository_head_tree(&head_tree, repo)) < 0 || + (error = git_diff_tree_to_index( + &index_diff, repo, head_tree, index, NULL)) < 0) + goto done; + + if (git_diff_num_deltas(index_diff) > 0) { + git_error_set(GIT_ERROR_STASH, "%" PRIuZ " uncommitted changes exist in the index", + git_diff_num_deltas(index_diff)); + error = GIT_EUNCOMMITTED; + } + +done: + git_diff_free(index_diff); + git_tree_free(head_tree); + return error; +} + +static int stage_new_file(const git_index_entry **entries, void *data) +{ + git_index *index = data; + + if(entries[0] == NULL) + return git_index_add(index, entries[1]); + else + return git_index_add(index, entries[0]); +} + +static int stage_new_files( + git_index **out, + git_repository *repo, + git_tree *parent_tree, + git_tree *tree) +{ + git_iterator *iterators[2] = { NULL, NULL }; + git_iterator_options iterator_options = GIT_ITERATOR_OPTIONS_INIT; + git_index *index = NULL; + int error; + + if ((error = git_index__new(&index, repo->oid_type)) < 0 || + (error = git_iterator_for_tree( + &iterators[0], parent_tree, &iterator_options)) < 0 || + (error = git_iterator_for_tree( + &iterators[1], tree, &iterator_options)) < 0) + goto done; + + error = git_iterator_walk(iterators, 2, stage_new_file, index); + +done: + if (error < 0) + git_index_free(index); + else + *out = index; + + git_iterator_free(iterators[0]); + git_iterator_free(iterators[1]); + + return error; +} + +int git_stash_apply( + git_repository *repo, + size_t index, + const git_stash_apply_options *given_opts) +{ + git_stash_apply_options opts; + unsigned int checkout_strategy; + git_commit *stash_commit = NULL; + git_tree *stash_tree = NULL; + git_tree *stash_parent_tree = NULL; + git_tree *index_tree = NULL; + git_tree *index_parent_tree = NULL; + git_tree *untracked_tree = NULL; + git_index *stash_adds = NULL; + git_index *repo_index = NULL; + git_index *unstashed_index = NULL; + git_index *modified_index = NULL; + git_index *untracked_index = NULL; + int error; + + GIT_ERROR_CHECK_VERSION(given_opts, GIT_STASH_APPLY_OPTIONS_VERSION, "git_stash_apply_options"); + + normalize_apply_options(&opts, given_opts); + checkout_strategy = opts.checkout_options.checkout_strategy; + + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_LOADING_STASH); + + /* Retrieve commit corresponding to the given stash */ + if ((error = retrieve_stash_commit(&stash_commit, repo, index)) < 0) + goto cleanup; + + /* Retrieve all trees in the stash */ + if ((error = retrieve_stash_trees( + &stash_tree, &stash_parent_tree, &index_tree, + &index_parent_tree, &untracked_tree, stash_commit)) < 0) + goto cleanup; + + /* Load repo index */ + if ((error = git_repository_index(&repo_index, repo)) < 0) + goto cleanup; + + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX); + + if ((error = ensure_clean_index(repo, repo_index)) < 0) + goto cleanup; + + /* Restore index if required */ + if ((opts.flags & GIT_STASH_APPLY_REINSTATE_INDEX) && + git_oid_cmp(git_tree_id(stash_parent_tree), git_tree_id(index_tree))) { + + if ((error = merge_index_and_tree( + &unstashed_index, repo, index_parent_tree, repo_index, index_tree)) < 0) + goto cleanup; + + if (git_index_has_conflicts(unstashed_index)) { + error = GIT_ECONFLICT; + goto cleanup; + } + + /* Otherwise, stage any new files in the stash tree. (Note: their + * previously unstaged contents are staged, not the previously staged.) + */ + } else if ((opts.flags & GIT_STASH_APPLY_REINSTATE_INDEX) == 0) { + if ((error = stage_new_files(&stash_adds, repo, + stash_parent_tree, stash_tree)) < 0 || + (error = merge_indexes(&unstashed_index, repo, + stash_parent_tree, repo_index, stash_adds)) < 0) + goto cleanup; + } + + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED); + + /* Restore modified files in workdir */ + if ((error = merge_index_and_tree( + &modified_index, repo, stash_parent_tree, repo_index, stash_tree)) < 0) + goto cleanup; + + /* If applicable, restore untracked / ignored files in workdir */ + if (untracked_tree) { + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED); + + if ((error = merge_index_and_tree(&untracked_index, repo, NULL, repo_index, untracked_tree)) < 0) + goto cleanup; + } + + if (untracked_index) { + opts.checkout_options.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; + + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED); + + if ((error = git_checkout_index(repo, untracked_index, &opts.checkout_options)) < 0) + goto cleanup; + + opts.checkout_options.checkout_strategy = checkout_strategy; + } + + + /* If there are conflicts in the modified index, then we need to actually + * check that out as the repo's index. Otherwise, we don't update the + * index. + */ + + if (!git_index_has_conflicts(modified_index)) + opts.checkout_options.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; + + /* Check out the modified index using the existing repo index as baseline, + * so that existing modifications in the index can be rewritten even when + * checking out safely. + */ + opts.checkout_options.baseline_index = repo_index; + + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED); + + if ((error = git_checkout_index(repo, modified_index, &opts.checkout_options)) < 0) + goto cleanup; + + if (unstashed_index && !git_index_has_conflicts(modified_index)) { + if ((error = git_index_read_index(repo_index, unstashed_index)) < 0) + goto cleanup; + } + + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_DONE); + + error = git_index_write(repo_index); + +cleanup: + git_index_free(untracked_index); + git_index_free(modified_index); + git_index_free(unstashed_index); + git_index_free(stash_adds); + git_index_free(repo_index); + git_tree_free(untracked_tree); + git_tree_free(index_parent_tree); + git_tree_free(index_tree); + git_tree_free(stash_parent_tree); + git_tree_free(stash_tree); + git_commit_free(stash_commit); + return error; +} + +int git_stash_foreach( + git_repository *repo, + git_stash_cb callback, + void *payload) +{ + git_reference *stash; + git_reflog *reflog = NULL; + int error; + size_t i, max; + const git_reflog_entry *entry; + + error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE); + if (error == GIT_ENOTFOUND) { + git_error_clear(); + return 0; + } + if (error < 0) + goto cleanup; + + if ((error = git_reflog_read(&reflog, repo, GIT_REFS_STASH_FILE)) < 0) + goto cleanup; + + max = git_reflog_entrycount(reflog); + for (i = 0; i < max; i++) { + entry = git_reflog_entry_byindex(reflog, i); + + error = callback(i, + git_reflog_entry_message(entry), + git_reflog_entry_id_new(entry), + payload); + + if (error) { + git_error_set_after_callback(error); + break; + } + } + +cleanup: + git_reference_free(stash); + git_reflog_free(reflog); + return error; +} + +int git_stash_drop( + git_repository *repo, + size_t index) +{ + git_transaction *tx; + git_reference *stash = NULL; + git_reflog *reflog = NULL; + size_t max; + int error; + + if ((error = git_transaction_new(&tx, repo)) < 0) + return error; + + if ((error = git_transaction_lock_ref(tx, GIT_REFS_STASH_FILE)) < 0) + goto cleanup; + + if ((error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE)) < 0) + goto cleanup; + + if ((error = git_reflog_read(&reflog, repo, GIT_REFS_STASH_FILE)) < 0) + goto cleanup; + + max = git_reflog_entrycount(reflog); + + if (!max || index > max - 1) { + error = GIT_ENOTFOUND; + git_error_set(GIT_ERROR_STASH, "no stashed state at position %" PRIuZ, index); + goto cleanup; + } + + if ((error = git_reflog_drop(reflog, index, true)) < 0) + goto cleanup; + + if ((error = git_transaction_set_reflog(tx, GIT_REFS_STASH_FILE, reflog)) < 0) + goto cleanup; + + if (max == 1) { + if ((error = git_transaction_remove(tx, GIT_REFS_STASH_FILE)) < 0) + goto cleanup; + } else if (index == 0) { + const git_reflog_entry *entry; + + entry = git_reflog_entry_byindex(reflog, 0); + if ((error = git_transaction_set_target(tx, GIT_REFS_STASH_FILE, &entry->oid_cur, NULL, NULL)) < 0) + goto cleanup; + } + + error = git_transaction_commit(tx); + +cleanup: + git_reference_free(stash); + git_transaction_free(tx); + git_reflog_free(reflog); + return error; +} + +int git_stash_pop( + git_repository *repo, + size_t index, + const git_stash_apply_options *options) +{ + int error; + + if ((error = git_stash_apply(repo, index, options)) < 0) + return error; + + return git_stash_drop(repo, index); +} diff --git a/src/libgit2/status.c b/src/libgit2/status.c new file mode 100644 index 0000000..df0f745 --- /dev/null +++ b/src/libgit2/status.c @@ -0,0 +1,584 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "status.h" + +#include "git2.h" +#include "futils.h" +#include "hash.h" +#include "vector.h" +#include "tree.h" +#include "git2/status.h" +#include "repository.h" +#include "ignore.h" +#include "index.h" +#include "wildmatch.h" + +#include "git2/diff.h" +#include "diff.h" +#include "diff_generate.h" + +static unsigned int index_delta2status(const git_diff_delta *head2idx) +{ + git_status_t st = GIT_STATUS_CURRENT; + + switch (head2idx->status) { + case GIT_DELTA_ADDED: + case GIT_DELTA_COPIED: + st = GIT_STATUS_INDEX_NEW; + break; + case GIT_DELTA_DELETED: + st = GIT_STATUS_INDEX_DELETED; + break; + case GIT_DELTA_MODIFIED: + st = GIT_STATUS_INDEX_MODIFIED; + break; + case GIT_DELTA_RENAMED: + st = GIT_STATUS_INDEX_RENAMED; + + if (!git_oid_equal(&head2idx->old_file.id, &head2idx->new_file.id)) + st |= GIT_STATUS_INDEX_MODIFIED; + break; + case GIT_DELTA_TYPECHANGE: + st = GIT_STATUS_INDEX_TYPECHANGE; + break; + case GIT_DELTA_CONFLICTED: + st = GIT_STATUS_CONFLICTED; + break; + default: + break; + } + + return st; +} + +static unsigned int workdir_delta2status( + git_diff *diff, git_diff_delta *idx2wd) +{ + git_status_t st = GIT_STATUS_CURRENT; + + switch (idx2wd->status) { + case GIT_DELTA_ADDED: + case GIT_DELTA_COPIED: + case GIT_DELTA_UNTRACKED: + st = GIT_STATUS_WT_NEW; + break; + case GIT_DELTA_UNREADABLE: + st = GIT_STATUS_WT_UNREADABLE; + break; + case GIT_DELTA_DELETED: + st = GIT_STATUS_WT_DELETED; + break; + case GIT_DELTA_MODIFIED: + st = GIT_STATUS_WT_MODIFIED; + break; + case GIT_DELTA_IGNORED: + st = GIT_STATUS_IGNORED; + break; + case GIT_DELTA_RENAMED: + st = GIT_STATUS_WT_RENAMED; + + if (!git_oid_equal(&idx2wd->old_file.id, &idx2wd->new_file.id)) { + /* if OIDs don't match, we might need to calculate them now to + * discern between RENAMED vs RENAMED+MODIFIED + */ + if (git_oid_is_zero(&idx2wd->old_file.id) && + diff->old_src == GIT_ITERATOR_WORKDIR && + !git_diff__oid_for_file( + &idx2wd->old_file.id, diff, idx2wd->old_file.path, + idx2wd->old_file.mode, idx2wd->old_file.size)) + idx2wd->old_file.flags |= GIT_DIFF_FLAG_VALID_ID; + + if (git_oid_is_zero(&idx2wd->new_file.id) && + diff->new_src == GIT_ITERATOR_WORKDIR && + !git_diff__oid_for_file( + &idx2wd->new_file.id, diff, idx2wd->new_file.path, + idx2wd->new_file.mode, idx2wd->new_file.size)) + idx2wd->new_file.flags |= GIT_DIFF_FLAG_VALID_ID; + + if (!git_oid_equal(&idx2wd->old_file.id, &idx2wd->new_file.id)) + st |= GIT_STATUS_WT_MODIFIED; + } + break; + case GIT_DELTA_TYPECHANGE: + st = GIT_STATUS_WT_TYPECHANGE; + break; + case GIT_DELTA_CONFLICTED: + st = GIT_STATUS_CONFLICTED; + break; + default: + break; + } + + return st; +} + +static bool status_is_included( + git_status_list *status, + git_diff_delta *head2idx, + git_diff_delta *idx2wd) +{ + if (!(status->opts.flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES)) + return 1; + + /* if excluding submodules and this is a submodule everywhere */ + if (head2idx) { + if (head2idx->status != GIT_DELTA_ADDED && + head2idx->old_file.mode != GIT_FILEMODE_COMMIT) + return 1; + if (head2idx->status != GIT_DELTA_DELETED && + head2idx->new_file.mode != GIT_FILEMODE_COMMIT) + return 1; + } + if (idx2wd) { + if (idx2wd->status != GIT_DELTA_ADDED && + idx2wd->old_file.mode != GIT_FILEMODE_COMMIT) + return 1; + if (idx2wd->status != GIT_DELTA_DELETED && + idx2wd->new_file.mode != GIT_FILEMODE_COMMIT) + return 1; + } + + /* only get here if every valid mode is GIT_FILEMODE_COMMIT */ + return 0; +} + +static git_status_t status_compute( + git_status_list *status, + git_diff_delta *head2idx, + git_diff_delta *idx2wd) +{ + git_status_t st = GIT_STATUS_CURRENT; + + if (head2idx) + st |= index_delta2status(head2idx); + + if (idx2wd) + st |= workdir_delta2status(status->idx2wd, idx2wd); + + return st; +} + +static int status_collect( + git_diff_delta *head2idx, + git_diff_delta *idx2wd, + void *payload) +{ + git_status_list *status = payload; + git_status_entry *status_entry; + + if (!status_is_included(status, head2idx, idx2wd)) + return 0; + + status_entry = git__malloc(sizeof(git_status_entry)); + GIT_ERROR_CHECK_ALLOC(status_entry); + + status_entry->status = status_compute(status, head2idx, idx2wd); + status_entry->head_to_index = head2idx; + status_entry->index_to_workdir = idx2wd; + + return git_vector_insert(&status->paired, status_entry); +} + +GIT_INLINE(int) status_entry_cmp_base( + const void *a, + const void *b, + int (*strcomp)(const char *a, const char *b)) +{ + const git_status_entry *entry_a = a; + const git_status_entry *entry_b = b; + const git_diff_delta *delta_a, *delta_b; + + delta_a = entry_a->index_to_workdir ? entry_a->index_to_workdir : + entry_a->head_to_index; + delta_b = entry_b->index_to_workdir ? entry_b->index_to_workdir : + entry_b->head_to_index; + + if (!delta_a && delta_b) + return -1; + if (delta_a && !delta_b) + return 1; + if (!delta_a && !delta_b) + return 0; + + return strcomp(delta_a->new_file.path, delta_b->new_file.path); +} + +static int status_entry_icmp(const void *a, const void *b) +{ + return status_entry_cmp_base(a, b, git__strcasecmp); +} + +static int status_entry_cmp(const void *a, const void *b) +{ + return status_entry_cmp_base(a, b, git__strcmp); +} + +static git_status_list *git_status_list_alloc(git_index *index) +{ + git_status_list *status = NULL; + int (*entrycmp)(const void *a, const void *b); + + if (!(status = git__calloc(1, sizeof(git_status_list)))) + return NULL; + + entrycmp = index->ignore_case ? status_entry_icmp : status_entry_cmp; + + if (git_vector_init(&status->paired, 0, entrycmp) < 0) { + git__free(status); + return NULL; + } + + return status; +} + +static int status_validate_options(const git_status_options *opts) +{ + if (!opts) + return 0; + + GIT_ERROR_CHECK_VERSION(opts, GIT_STATUS_OPTIONS_VERSION, "git_status_options"); + + if (opts->show > GIT_STATUS_SHOW_WORKDIR_ONLY) { + git_error_set(GIT_ERROR_INVALID, "unknown status 'show' option"); + return -1; + } + + if ((opts->flags & GIT_STATUS_OPT_NO_REFRESH) != 0 && + (opts->flags & GIT_STATUS_OPT_UPDATE_INDEX) != 0) { + git_error_set(GIT_ERROR_INVALID, "updating index from status " + "is not allowed when index refresh is disabled"); + return -1; + } + + return 0; +} + +int git_status_list_new( + git_status_list **out, + git_repository *repo, + const git_status_options *opts) +{ + git_index *index = NULL; + git_status_list *status = NULL; + git_diff_options diffopt = GIT_DIFF_OPTIONS_INIT; + git_diff_find_options findopt = GIT_DIFF_FIND_OPTIONS_INIT; + git_tree *head = NULL; + git_status_show_t show = + opts ? opts->show : GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + int error = 0; + unsigned int flags = opts ? opts->flags : GIT_STATUS_OPT_DEFAULTS; + + *out = NULL; + + if (status_validate_options(opts) < 0) + return -1; + + if ((error = git_repository__ensure_not_bare(repo, "status")) < 0 || + (error = git_repository_index(&index, repo)) < 0) + return error; + + if (opts != NULL && opts->baseline != NULL) { + head = opts->baseline; + } else { + /* if there is no HEAD, that's okay - we'll make an empty iterator */ + if ((error = git_repository_head_tree(&head, repo)) < 0) { + if (error != GIT_ENOTFOUND && error != GIT_EUNBORNBRANCH) + goto done; + git_error_clear(); + } + } + + /* refresh index from disk unless prevented */ + if ((flags & GIT_STATUS_OPT_NO_REFRESH) == 0 && + git_index_read_safely(index) < 0) + git_error_clear(); + + status = git_status_list_alloc(index); + GIT_ERROR_CHECK_ALLOC(status); + + if (opts) { + memcpy(&status->opts, opts, sizeof(git_status_options)); + memcpy(&diffopt.pathspec, &opts->pathspec, sizeof(diffopt.pathspec)); + } + + diffopt.flags = GIT_DIFF_INCLUDE_TYPECHANGE; + findopt.flags = GIT_DIFF_FIND_FOR_UNTRACKED; + + if ((flags & GIT_STATUS_OPT_INCLUDE_UNTRACKED) != 0) + diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNTRACKED; + if ((flags & GIT_STATUS_OPT_INCLUDE_IGNORED) != 0) + diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_IGNORED; + if ((flags & GIT_STATUS_OPT_INCLUDE_UNMODIFIED) != 0) + diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNMODIFIED; + if ((flags & GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS) != 0) + diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_UNTRACKED_DIRS; + if ((flags & GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH) != 0) + diffopt.flags = diffopt.flags | GIT_DIFF_DISABLE_PATHSPEC_MATCH; + if ((flags & GIT_STATUS_OPT_RECURSE_IGNORED_DIRS) != 0) + diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_IGNORED_DIRS; + if ((flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0) + diffopt.flags = diffopt.flags | GIT_DIFF_IGNORE_SUBMODULES; + if ((flags & GIT_STATUS_OPT_UPDATE_INDEX) != 0) + diffopt.flags = diffopt.flags | GIT_DIFF_UPDATE_INDEX; + if ((flags & GIT_STATUS_OPT_INCLUDE_UNREADABLE) != 0) + diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNREADABLE; + if ((flags & GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED) != 0) + diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED; + + if ((flags & GIT_STATUS_OPT_RENAMES_FROM_REWRITES) != 0) + findopt.flags = findopt.flags | + GIT_DIFF_FIND_AND_BREAK_REWRITES | + GIT_DIFF_FIND_RENAMES_FROM_REWRITES | + GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY; + + if (opts != NULL && opts->rename_threshold != 0) + findopt.rename_threshold = opts->rename_threshold; + + if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) { + if ((error = git_diff_tree_to_index( + &status->head2idx, repo, head, index, &diffopt)) < 0) + goto done; + + if ((flags & GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX) != 0 && + (error = git_diff_find_similar(status->head2idx, &findopt)) < 0) + goto done; + } + + if (show != GIT_STATUS_SHOW_INDEX_ONLY) { + if ((error = git_diff_index_to_workdir( + &status->idx2wd, repo, index, &diffopt)) < 0) { + goto done; + } + + if ((flags & GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR) != 0 && + (error = git_diff_find_similar(status->idx2wd, &findopt)) < 0) + goto done; + } + + error = git_diff__paired_foreach( + status->head2idx, status->idx2wd, status_collect, status); + if (error < 0) + goto done; + + if (flags & GIT_STATUS_OPT_SORT_CASE_SENSITIVELY) + git_vector_set_cmp(&status->paired, status_entry_cmp); + if (flags & GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY) + git_vector_set_cmp(&status->paired, status_entry_icmp); + + if ((flags & + (GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | + GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR | + GIT_STATUS_OPT_SORT_CASE_SENSITIVELY | + GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY)) != 0) + git_vector_sort(&status->paired); + +done: + if (error < 0) { + git_status_list_free(status); + status = NULL; + } + + *out = status; + + if (opts == NULL || opts->baseline != head) + git_tree_free(head); + git_index_free(index); + + return error; +} + +size_t git_status_list_entrycount(git_status_list *status) +{ + GIT_ASSERT_ARG_WITH_RETVAL(status, 0); + + return status->paired.length; +} + +const git_status_entry *git_status_byindex(git_status_list *status, size_t i) +{ + GIT_ASSERT_ARG_WITH_RETVAL(status, NULL); + + return git_vector_get(&status->paired, i); +} + +void git_status_list_free(git_status_list *status) +{ + if (status == NULL) + return; + + git_diff_free(status->head2idx); + git_diff_free(status->idx2wd); + + git_vector_free_deep(&status->paired); + + git__memzero(status, sizeof(*status)); + git__free(status); +} + +int git_status_foreach_ext( + git_repository *repo, + const git_status_options *opts, + git_status_cb cb, + void *payload) +{ + git_status_list *status; + const git_status_entry *status_entry; + size_t i; + int error = 0; + + if ((error = git_status_list_new(&status, repo, opts)) < 0) { + return error; + } + + git_vector_foreach(&status->paired, i, status_entry) { + const char *path = status_entry->head_to_index ? + status_entry->head_to_index->old_file.path : + status_entry->index_to_workdir->old_file.path; + + if ((error = cb(path, status_entry->status, payload)) != 0) { + git_error_set_after_callback(error); + break; + } + } + + git_status_list_free(status); + + return error; +} + +int git_status_foreach(git_repository *repo, git_status_cb cb, void *payload) +{ + return git_status_foreach_ext(repo, NULL, cb, payload); +} + +struct status_file_info { + char *expected; + unsigned int count; + unsigned int status; + int wildmatch_flags; + int ambiguous; +}; + +static int get_one_status(const char *path, unsigned int status, void *data) +{ + struct status_file_info *sfi = data; + int (*strcomp)(const char *a, const char *b); + + sfi->count++; + sfi->status = status; + + strcomp = (sfi->wildmatch_flags & WM_CASEFOLD) ? git__strcasecmp : git__strcmp; + + if (sfi->count > 1 || + (strcomp(sfi->expected, path) != 0 && + wildmatch(sfi->expected, path, sfi->wildmatch_flags) != 0)) + { + sfi->ambiguous = true; + return GIT_EAMBIGUOUS; /* git_error_set will be done by caller */ + } + + return 0; +} + +int git_status_file( + unsigned int *status_flags, + git_repository *repo, + const char *path) +{ + int error; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_file_info sfi = {0}; + git_index *index; + + GIT_ASSERT_ARG(status_flags); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(path); + + if ((error = git_repository_index__weakptr(&index, repo)) < 0) + return error; + + if ((sfi.expected = git__strdup(path)) == NULL) + return -1; + if (index->ignore_case) + sfi.wildmatch_flags = WM_CASEFOLD; + + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED | + GIT_STATUS_OPT_RECURSE_IGNORED_DIRS | + GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS | + GIT_STATUS_OPT_INCLUDE_UNMODIFIED | + GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH; + opts.pathspec.count = 1; + opts.pathspec.strings = &sfi.expected; + + error = git_status_foreach_ext(repo, &opts, get_one_status, &sfi); + + if (error < 0 && sfi.ambiguous) { + git_error_set(GIT_ERROR_INVALID, + "ambiguous path '%s' given to git_status_file", sfi.expected); + error = GIT_EAMBIGUOUS; + } + + if (!error && !sfi.count) { + git_error_set(GIT_ERROR_INVALID, + "attempt to get status of nonexistent file '%s'", path); + error = GIT_ENOTFOUND; + } + + *status_flags = sfi.status; + + git__free(sfi.expected); + + return error; +} + +int git_status_should_ignore( + int *ignored, + git_repository *repo, + const char *path) +{ + return git_ignore_path_is_ignored(ignored, repo, path); +} + +int git_status_options_init(git_status_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_status_options, GIT_STATUS_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_status_init_options(git_status_options *opts, unsigned int version) +{ + return git_status_options_init(opts, version); +} +#endif + +int git_status_list_get_perfdata( + git_diff_perfdata *out, const git_status_list *status) +{ + GIT_ASSERT_ARG(out); + + GIT_ERROR_CHECK_VERSION(out, GIT_DIFF_PERFDATA_VERSION, "git_diff_perfdata"); + + out->stat_calls = 0; + out->oid_calculations = 0; + + if (status->head2idx) { + out->stat_calls += status->head2idx->perf.stat_calls; + out->oid_calculations += status->head2idx->perf.oid_calculations; + } + if (status->idx2wd) { + out->stat_calls += status->idx2wd->perf.stat_calls; + out->oid_calculations += status->idx2wd->perf.oid_calculations; + } + + return 0; +} + diff --git a/src/libgit2/status.h b/src/libgit2/status.h new file mode 100644 index 0000000..907479a --- /dev/null +++ b/src/libgit2/status.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_status_h__ +#define INCLUDE_status_h__ + +#include "common.h" + +#include "diff.h" +#include "git2/status.h" +#include "git2/diff.h" + +struct git_status_list { + git_status_options opts; + + git_diff *head2idx; + git_diff *idx2wd; + + git_vector paired; +}; + +#endif diff --git a/src/libgit2/strarray.c b/src/libgit2/strarray.c new file mode 100644 index 0000000..25e75f0 --- /dev/null +++ b/src/libgit2/strarray.c @@ -0,0 +1,65 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "util.h" + +#include "common.h" +#include "strarray.h" + +int git_strarray_copy(git_strarray *tgt, const git_strarray *src) +{ + size_t i; + + GIT_ASSERT_ARG(tgt); + GIT_ASSERT_ARG(src); + + memset(tgt, 0, sizeof(*tgt)); + + if (!src->count) + return 0; + + tgt->strings = git__calloc(src->count, sizeof(char *)); + GIT_ERROR_CHECK_ALLOC(tgt->strings); + + for (i = 0; i < src->count; ++i) { + if (!src->strings[i]) + continue; + + tgt->strings[tgt->count] = git__strdup(src->strings[i]); + if (!tgt->strings[tgt->count]) { + git_strarray_dispose(tgt); + memset(tgt, 0, sizeof(*tgt)); + return -1; + } + + tgt->count++; + } + + return 0; +} + +void git_strarray_dispose(git_strarray *array) +{ + size_t i; + + if (array == NULL) + return; + + for (i = 0; i < array->count; ++i) + git__free(array->strings[i]); + + git__free(array->strings); + + memset(array, 0, sizeof(*array)); +} + +#ifndef GIT_DEPRECATE_HARD +void git_strarray_free(git_strarray *array) +{ + git_strarray_dispose(array); +} +#endif diff --git a/src/libgit2/strarray.h b/src/libgit2/strarray.h new file mode 100644 index 0000000..1984805 --- /dev/null +++ b/src/libgit2/strarray.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_strarray_h__ +#define INCLUDE_strarray_h__ + +#include "common.h" +#include "git2/strarray.h" + +/** + * Copy a string array object from source to target. + * + * Note: target is overwritten and hence should be empty, otherwise its + * contents are leaked. Call git_strarray_free() if necessary. + * + * @param tgt target + * @param src source + * @return 0 on success, < 0 on allocation failure + */ +extern int git_strarray_copy(git_strarray *tgt, const git_strarray *src); + +#endif diff --git a/src/libgit2/stream.h b/src/libgit2/stream.h new file mode 100644 index 0000000..f16b026 --- /dev/null +++ b/src/libgit2/stream.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_stream_h__ +#define INCLUDE_stream_h__ + +#include "common.h" +#include "git2/sys/stream.h" + +GIT_INLINE(int) git_stream_connect(git_stream *st) +{ + return st->connect(st); +} + +GIT_INLINE(int) git_stream_is_encrypted(git_stream *st) +{ + return st->encrypted; +} + +GIT_INLINE(int) git_stream_certificate(git_cert **out, git_stream *st) +{ + if (!st->encrypted) { + git_error_set(GIT_ERROR_INVALID, "an unencrypted stream does not have a certificate"); + return -1; + } + + return st->certificate(out, st); +} + +GIT_INLINE(int) git_stream_supports_proxy(git_stream *st) +{ + return st->proxy_support; +} + +GIT_INLINE(int) git_stream_set_proxy(git_stream *st, const git_proxy_options *proxy_opts) +{ + if (!st->proxy_support) { + git_error_set(GIT_ERROR_INVALID, "proxy not supported on this stream"); + return -1; + } + + return st->set_proxy(st, proxy_opts); +} + +GIT_INLINE(ssize_t) git_stream_read(git_stream *st, void *data, size_t len) +{ + return st->read(st, data, len); +} + +GIT_INLINE(ssize_t) git_stream_write(git_stream *st, const char *data, size_t len, int flags) +{ + return st->write(st, data, len, flags); +} + +GIT_INLINE(int) git_stream__write_full(git_stream *st, const char *data, size_t len, int flags) +{ + size_t total_written = 0; + + while (total_written < len) { + ssize_t written = git_stream_write(st, data + total_written, len - total_written, flags); + if (written <= 0) + return -1; + + total_written += written; + } + + return 0; +} + +GIT_INLINE(int) git_stream_close(git_stream *st) +{ + return st->close(st); +} + +GIT_INLINE(void) git_stream_free(git_stream *st) +{ + if (!st) + return; + + st->free(st); +} + +#endif diff --git a/src/libgit2/streams/mbedtls.c b/src/libgit2/streams/mbedtls.c new file mode 100644 index 0000000..49aa76c --- /dev/null +++ b/src/libgit2/streams/mbedtls.c @@ -0,0 +1,481 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "streams/mbedtls.h" + +#ifdef GIT_MBEDTLS + +#include + +#include "runtime.h" +#include "stream.h" +#include "streams/socket.h" +#include "git2/transport.h" +#include "util.h" + +#ifndef GIT_DEFAULT_CERT_LOCATION +#define GIT_DEFAULT_CERT_LOCATION NULL +#endif + +/* Work around C90-conformance issues */ +#if !defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) +# if defined(_MSC_VER) +# define inline __inline +# elif defined(__GNUC__) +# define inline __inline__ +# else +# define inline +# endif +#endif + +#include +#include +#include +#include +#include + +#undef inline + +#define GIT_SSL_DEFAULT_CIPHERS "TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384:TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-128-GCM-SHA256:TLS-DHE-DSS-WITH-AES-128-GCM-SHA256:TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-DSS-WITH-AES-256-GCM-SHA384:TLS-ECDHE-ECDSA-WITH-AES-128-CBC-SHA256:TLS-ECDHE-RSA-WITH-AES-128-CBC-SHA256:TLS-ECDHE-ECDSA-WITH-AES-128-CBC-SHA:TLS-ECDHE-RSA-WITH-AES-128-CBC-SHA:TLS-ECDHE-ECDSA-WITH-AES-256-CBC-SHA384:TLS-ECDHE-RSA-WITH-AES-256-CBC-SHA384:TLS-ECDHE-ECDSA-WITH-AES-256-CBC-SHA:TLS-ECDHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-RSA-WITH-AES-128-CBC-SHA256:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256:TLS-DHE-RSA-WITH-AES-128-CBC-SHA:TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-DSS-WITH-AES-128-CBC-SHA256:TLS-DHE-DSS-WITH-AES-256-CBC-SHA256:TLS-DHE-DSS-WITH-AES-128-CBC-SHA:TLS-DHE-DSS-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-AES-128-GCM-SHA256:TLS-RSA-WITH-AES-256-GCM-SHA384:TLS-RSA-WITH-AES-128-CBC-SHA256:TLS-RSA-WITH-AES-256-CBC-SHA256:TLS-RSA-WITH-AES-128-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA" +#define GIT_SSL_DEFAULT_CIPHERS_COUNT 30 + +static mbedtls_ssl_config *git__ssl_conf; +static int ciphers_list[GIT_SSL_DEFAULT_CIPHERS_COUNT]; +static mbedtls_entropy_context *mbedtls_entropy; + +/** + * This function aims to clean-up the SSL context which + * we allocated. + */ +static void shutdown_ssl(void) +{ + if (git__ssl_conf) { + mbedtls_x509_crt_free(git__ssl_conf->ca_chain); + git__free(git__ssl_conf->ca_chain); + mbedtls_ctr_drbg_free(git__ssl_conf->p_rng); + git__free(git__ssl_conf->p_rng); + mbedtls_ssl_config_free(git__ssl_conf); + git__free(git__ssl_conf); + git__ssl_conf = NULL; + } + if (mbedtls_entropy) { + mbedtls_entropy_free(mbedtls_entropy); + git__free(mbedtls_entropy); + mbedtls_entropy = NULL; + } +} + +int git_mbedtls_stream_global_init(void) +{ + int loaded = 0; + char *crtpath = GIT_DEFAULT_CERT_LOCATION; + struct stat statbuf; + mbedtls_ctr_drbg_context *ctr_drbg = NULL; + + size_t ciphers_known = 0; + char *cipher_name = NULL; + char *cipher_string = NULL; + char *cipher_string_tmp = NULL; + + git__ssl_conf = git__malloc(sizeof(mbedtls_ssl_config)); + GIT_ERROR_CHECK_ALLOC(git__ssl_conf); + + mbedtls_ssl_config_init(git__ssl_conf); + if (mbedtls_ssl_config_defaults(git__ssl_conf, + MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT) != 0) { + git_error_set(GIT_ERROR_SSL, "failed to initialize mbedTLS"); + goto cleanup; + } + + /* configure TLSv1 */ + mbedtls_ssl_conf_min_version(git__ssl_conf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_0); + + /* verify_server_cert is responsible for making the check. + * OPTIONAL because REQUIRED drops the certificate as soon as the check + * is made, so we can never see the certificate and override it. */ + mbedtls_ssl_conf_authmode(git__ssl_conf, MBEDTLS_SSL_VERIFY_OPTIONAL); + + /* set the list of allowed ciphersuites */ + ciphers_known = 0; + cipher_string = cipher_string_tmp = git__strdup(GIT_SSL_DEFAULT_CIPHERS); + GIT_ERROR_CHECK_ALLOC(cipher_string); + + while ((cipher_name = git__strtok(&cipher_string_tmp, ":")) != NULL) { + int cipherid = mbedtls_ssl_get_ciphersuite_id(cipher_name); + if (cipherid == 0) continue; + + if (ciphers_known >= ARRAY_SIZE(ciphers_list)) { + git_error_set(GIT_ERROR_SSL, "out of cipher list space"); + goto cleanup; + } + + ciphers_list[ciphers_known++] = cipherid; + } + git__free(cipher_string); + + if (!ciphers_known) { + git_error_set(GIT_ERROR_SSL, "no cipher could be enabled"); + goto cleanup; + } + mbedtls_ssl_conf_ciphersuites(git__ssl_conf, ciphers_list); + + /* Seeding the random number generator */ + mbedtls_entropy = git__malloc(sizeof(mbedtls_entropy_context)); + GIT_ERROR_CHECK_ALLOC(mbedtls_entropy); + + mbedtls_entropy_init(mbedtls_entropy); + + ctr_drbg = git__malloc(sizeof(mbedtls_ctr_drbg_context)); + GIT_ERROR_CHECK_ALLOC(ctr_drbg); + + mbedtls_ctr_drbg_init(ctr_drbg); + + if (mbedtls_ctr_drbg_seed(ctr_drbg, + mbedtls_entropy_func, + mbedtls_entropy, NULL, 0) != 0) { + git_error_set(GIT_ERROR_SSL, "failed to initialize mbedTLS entropy pool"); + goto cleanup; + } + + mbedtls_ssl_conf_rng(git__ssl_conf, mbedtls_ctr_drbg_random, ctr_drbg); + + /* load default certificates */ + if (crtpath != NULL && stat(crtpath, &statbuf) == 0 && S_ISREG(statbuf.st_mode)) + loaded = (git_mbedtls__set_cert_location(crtpath, NULL) == 0); + if (!loaded && crtpath != NULL && stat(crtpath, &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) + loaded = (git_mbedtls__set_cert_location(NULL, crtpath) == 0); + + return git_runtime_shutdown_register(shutdown_ssl); + +cleanup: + mbedtls_ctr_drbg_free(ctr_drbg); + git__free(ctr_drbg); + mbedtls_ssl_config_free(git__ssl_conf); + git__free(git__ssl_conf); + git__ssl_conf = NULL; + + return -1; +} + +static int bio_read(void *b, unsigned char *buf, size_t len) +{ + git_stream *io = (git_stream *) b; + return (int) git_stream_read(io, buf, min(len, INT_MAX)); +} + +static int bio_write(void *b, const unsigned char *buf, size_t len) +{ + git_stream *io = (git_stream *) b; + return (int) git_stream_write(io, (const char *)buf, min(len, INT_MAX), 0); +} + +static int ssl_set_error(mbedtls_ssl_context *ssl, int error) +{ + char errbuf[512]; + int ret = -1; + + GIT_ASSERT(error != MBEDTLS_ERR_SSL_WANT_READ); + GIT_ASSERT(error != MBEDTLS_ERR_SSL_WANT_WRITE); + + if (error != 0) + mbedtls_strerror( error, errbuf, 512 ); + + switch(error) { + case 0: + git_error_set(GIT_ERROR_SSL, "SSL error: unknown error"); + break; + + case MBEDTLS_ERR_X509_CERT_VERIFY_FAILED: + git_error_set(GIT_ERROR_SSL, "SSL error: %#04x [%x] - %s", error, ssl->session_negotiate->verify_result, errbuf); + ret = GIT_ECERTIFICATE; + break; + + default: + git_error_set(GIT_ERROR_SSL, "SSL error: %#04x - %s", error, errbuf); + } + + return ret; +} + +static int ssl_teardown(mbedtls_ssl_context *ssl) +{ + int ret = 0; + + ret = mbedtls_ssl_close_notify(ssl); + if (ret < 0) + ret = ssl_set_error(ssl, ret); + + mbedtls_ssl_free(ssl); + return ret; +} + +static int verify_server_cert(mbedtls_ssl_context *ssl) +{ + int ret = -1; + + if ((ret = mbedtls_ssl_get_verify_result(ssl)) != 0) { + char vrfy_buf[512]; + int len = mbedtls_x509_crt_verify_info(vrfy_buf, sizeof(vrfy_buf), "", ret); + if (len >= 1) vrfy_buf[len - 1] = '\0'; /* Remove trailing \n */ + git_error_set(GIT_ERROR_SSL, "the SSL certificate is invalid: %#04x - %s", ret, vrfy_buf); + return GIT_ECERTIFICATE; + } + + return 0; +} + +typedef struct { + git_stream parent; + git_stream *io; + int owned; + bool connected; + char *host; + mbedtls_ssl_context *ssl; + git_cert_x509 cert_info; +} mbedtls_stream; + + +static int mbedtls_connect(git_stream *stream) +{ + int ret; + mbedtls_stream *st = (mbedtls_stream *) stream; + + if (st->owned && (ret = git_stream_connect(st->io)) < 0) + return ret; + + st->connected = true; + + mbedtls_ssl_set_hostname(st->ssl, st->host); + + mbedtls_ssl_set_bio(st->ssl, st->io, bio_write, bio_read, NULL); + + if ((ret = mbedtls_ssl_handshake(st->ssl)) != 0) + return ssl_set_error(st->ssl, ret); + + return verify_server_cert(st->ssl); +} + +static int mbedtls_certificate(git_cert **out, git_stream *stream) +{ + unsigned char *encoded_cert; + mbedtls_stream *st = (mbedtls_stream *) stream; + + const mbedtls_x509_crt *cert = mbedtls_ssl_get_peer_cert(st->ssl); + if (!cert) { + git_error_set(GIT_ERROR_SSL, "the server did not provide a certificate"); + return -1; + } + + /* Retrieve the length of the certificate first */ + if (cert->raw.len == 0) { + git_error_set(GIT_ERROR_NET, "failed to retrieve certificate information"); + return -1; + } + + encoded_cert = git__malloc(cert->raw.len); + GIT_ERROR_CHECK_ALLOC(encoded_cert); + memcpy(encoded_cert, cert->raw.p, cert->raw.len); + + st->cert_info.parent.cert_type = GIT_CERT_X509; + st->cert_info.data = encoded_cert; + st->cert_info.len = cert->raw.len; + + *out = &st->cert_info.parent; + + return 0; +} + +static int mbedtls_set_proxy(git_stream *stream, const git_proxy_options *proxy_options) +{ + mbedtls_stream *st = (mbedtls_stream *) stream; + + return git_stream_set_proxy(st->io, proxy_options); +} + +static ssize_t mbedtls_stream_write(git_stream *stream, const char *data, size_t len, int flags) +{ + mbedtls_stream *st = (mbedtls_stream *) stream; + int written; + + GIT_UNUSED(flags); + + /* + * `mbedtls_ssl_write` can only represent INT_MAX bytes + * written via its return value. We thus need to clamp + * the maximum number of bytes written. + */ + len = min(len, INT_MAX); + + if ((written = mbedtls_ssl_write(st->ssl, (const unsigned char *)data, len)) <= 0) + return ssl_set_error(st->ssl, written); + + return written; +} + +static ssize_t mbedtls_stream_read(git_stream *stream, void *data, size_t len) +{ + mbedtls_stream *st = (mbedtls_stream *) stream; + int ret; + + if ((ret = mbedtls_ssl_read(st->ssl, (unsigned char *)data, len)) <= 0) + ssl_set_error(st->ssl, ret); + + return ret; +} + +static int mbedtls_stream_close(git_stream *stream) +{ + mbedtls_stream *st = (mbedtls_stream *) stream; + int ret = 0; + + if (st->connected && (ret = ssl_teardown(st->ssl)) != 0) + return -1; + + st->connected = false; + + return st->owned ? git_stream_close(st->io) : 0; +} + +static void mbedtls_stream_free(git_stream *stream) +{ + mbedtls_stream *st = (mbedtls_stream *) stream; + + if (st->owned) + git_stream_free(st->io); + + git__free(st->host); + git__free(st->cert_info.data); + mbedtls_ssl_free(st->ssl); + git__free(st->ssl); + git__free(st); +} + +static int mbedtls_stream_wrap( + git_stream **out, + git_stream *in, + const char *host, + int owned) +{ + mbedtls_stream *st; + int error; + + st = git__calloc(1, sizeof(mbedtls_stream)); + GIT_ERROR_CHECK_ALLOC(st); + + st->io = in; + st->owned = owned; + + st->ssl = git__malloc(sizeof(mbedtls_ssl_context)); + GIT_ERROR_CHECK_ALLOC(st->ssl); + mbedtls_ssl_init(st->ssl); + if (mbedtls_ssl_setup(st->ssl, git__ssl_conf)) { + git_error_set(GIT_ERROR_SSL, "failed to create ssl object"); + error = -1; + goto out_err; + } + + st->host = git__strdup(host); + GIT_ERROR_CHECK_ALLOC(st->host); + + st->parent.version = GIT_STREAM_VERSION; + st->parent.encrypted = 1; + st->parent.proxy_support = git_stream_supports_proxy(st->io); + st->parent.connect = mbedtls_connect; + st->parent.certificate = mbedtls_certificate; + st->parent.set_proxy = mbedtls_set_proxy; + st->parent.read = mbedtls_stream_read; + st->parent.write = mbedtls_stream_write; + st->parent.close = mbedtls_stream_close; + st->parent.free = mbedtls_stream_free; + + *out = (git_stream *) st; + return 0; + +out_err: + mbedtls_ssl_free(st->ssl); + git_stream_close(st->io); + git_stream_free(st->io); + git__free(st); + + return error; +} + +int git_mbedtls_stream_wrap( + git_stream **out, + git_stream *in, + const char *host) +{ + return mbedtls_stream_wrap(out, in, host, 0); +} + +int git_mbedtls_stream_new( + git_stream **out, + const char *host, + const char *port) +{ + git_stream *stream; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(host); + GIT_ASSERT_ARG(port); + + if ((error = git_socket_stream_new(&stream, host, port)) < 0) + return error; + + if ((error = mbedtls_stream_wrap(out, stream, host, 1)) < 0) { + git_stream_close(stream); + git_stream_free(stream); + } + + return error; +} + +int git_mbedtls__set_cert_location(const char *file, const char *path) +{ + int ret = 0; + char errbuf[512]; + mbedtls_x509_crt *cacert; + + GIT_ASSERT_ARG(file || path); + + cacert = git__malloc(sizeof(mbedtls_x509_crt)); + GIT_ERROR_CHECK_ALLOC(cacert); + + mbedtls_x509_crt_init(cacert); + if (file) + ret = mbedtls_x509_crt_parse_file(cacert, file); + if (ret >= 0 && path) + ret = mbedtls_x509_crt_parse_path(cacert, path); + /* mbedtls_x509_crt_parse_path returns the number of invalid certs on success */ + if (ret < 0) { + mbedtls_x509_crt_free(cacert); + git__free(cacert); + mbedtls_strerror( ret, errbuf, 512 ); + git_error_set(GIT_ERROR_SSL, "failed to load CA certificates: %#04x - %s", ret, errbuf); + return -1; + } + + mbedtls_x509_crt_free(git__ssl_conf->ca_chain); + git__free(git__ssl_conf->ca_chain); + mbedtls_ssl_conf_ca_chain(git__ssl_conf, cacert, NULL); + + return 0; +} + +#else + +#include "stream.h" + +int git_mbedtls_stream_global_init(void) +{ + return 0; +} + +#endif diff --git a/src/libgit2/streams/mbedtls.h b/src/libgit2/streams/mbedtls.h new file mode 100644 index 0000000..bcca6dd --- /dev/null +++ b/src/libgit2/streams/mbedtls.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_steams_mbedtls_h__ +#define INCLUDE_steams_mbedtls_h__ + +#include "common.h" + +#include "git2/sys/stream.h" + +extern int git_mbedtls_stream_global_init(void); + +#ifdef GIT_MBEDTLS +extern int git_mbedtls__set_cert_location(const char *file, const char *path); + +extern int git_mbedtls_stream_new(git_stream **out, const char *host, const char *port); +extern int git_mbedtls_stream_wrap(git_stream **out, git_stream *in, const char *host); +#endif + +#endif diff --git a/src/libgit2/streams/openssl.c b/src/libgit2/streams/openssl.c new file mode 100644 index 0000000..9db911e --- /dev/null +++ b/src/libgit2/streams/openssl.c @@ -0,0 +1,739 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "streams/openssl.h" +#include "streams/openssl_legacy.h" +#include "streams/openssl_dynamic.h" + +#ifdef GIT_OPENSSL + +#include + +#include "common.h" +#include "runtime.h" +#include "settings.h" +#include "posix.h" +#include "stream.h" +#include "net.h" +#include "streams/socket.h" +#include "git2/transport.h" +#include "git2/sys/openssl.h" + +#ifndef GIT_WIN32 +# include +# include +# include +#endif + +#ifndef GIT_OPENSSL_DYNAMIC +# include +# include +# include +# include +#endif + +SSL_CTX *git__ssl_ctx; + +#define GIT_SSL_DEFAULT_CIPHERS "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES128-SHA256:DHE-DSS-AES256-SHA256:DHE-DSS-AES128-SHA:DHE-DSS-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA" + + +static BIO_METHOD *git_stream_bio_method; +static int init_bio_method(void); + +/** + * This function aims to clean-up the SSL context which + * we allocated. + */ +static void shutdown_ssl(void) +{ + if (git_stream_bio_method) { + BIO_meth_free(git_stream_bio_method); + git_stream_bio_method = NULL; + } + + if (git__ssl_ctx) { + SSL_CTX_free(git__ssl_ctx); + git__ssl_ctx = NULL; + } +} + +#ifdef VALGRIND +# if !defined(GIT_OPENSSL_LEGACY) && !defined(GIT_OPENSSL_DYNAMIC) + +static void *git_openssl_malloc(size_t bytes, const char *file, int line) +{ + GIT_UNUSED(file); + GIT_UNUSED(line); + return git__calloc(1, bytes); +} + +static void *git_openssl_realloc(void *mem, size_t size, const char *file, int line) +{ + GIT_UNUSED(file); + GIT_UNUSED(line); + return git__realloc(mem, size); +} + +static void git_openssl_free(void *mem, const char *file, int line) +{ + GIT_UNUSED(file); + GIT_UNUSED(line); + git__free(mem); +} +# else /* !GIT_OPENSSL_LEGACY && !GIT_OPENSSL_DYNAMIC */ +static void *git_openssl_malloc(size_t bytes) +{ + return git__calloc(1, bytes); +} + +static void *git_openssl_realloc(void *mem, size_t size) +{ + return git__realloc(mem, size); +} + +static void git_openssl_free(void *mem) +{ + git__free(mem); +} +# endif /* !GIT_OPENSSL_LEGACY && !GIT_OPENSSL_DYNAMIC */ +#endif /* VALGRIND */ + +static int openssl_init(void) +{ + long ssl_opts = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3; + const char *ciphers = git_libgit2__ssl_ciphers(); +#ifdef VALGRIND + static bool allocators_initialized = false; +#endif + + /* Older OpenSSL and MacOS OpenSSL doesn't have this */ +#ifdef SSL_OP_NO_COMPRESSION + ssl_opts |= SSL_OP_NO_COMPRESSION; +#endif + +#ifdef VALGRIND + /* + * Swap in our own allocator functions that initialize + * allocated memory to avoid spurious valgrind warnings. + * Don't error on failure; many builds of OpenSSL do not + * allow you to set these functions. + */ + if (!allocators_initialized) { + CRYPTO_set_mem_functions(git_openssl_malloc, + git_openssl_realloc, + git_openssl_free); + allocators_initialized = true; + } +#endif + + OPENSSL_init_ssl(0, NULL); + + /* + * Load SSLv{2,3} and TLSv1 so that we can talk with servers + * which use the SSL hellos, which are often used for + * compatibility. We then disable SSL so we only allow OpenSSL + * to speak TLSv1 to perform the encryption itself. + */ + if (!(git__ssl_ctx = SSL_CTX_new(SSLv23_method()))) + goto error; + + SSL_CTX_set_options(git__ssl_ctx, ssl_opts); + SSL_CTX_set_mode(git__ssl_ctx, SSL_MODE_AUTO_RETRY); + SSL_CTX_set_verify(git__ssl_ctx, SSL_VERIFY_NONE, NULL); + if (!SSL_CTX_set_default_verify_paths(git__ssl_ctx)) + goto error; + + if (!ciphers) + ciphers = GIT_SSL_DEFAULT_CIPHERS; + + if(!SSL_CTX_set_cipher_list(git__ssl_ctx, ciphers)) + goto error; + + if (init_bio_method() < 0) + goto error; + + return git_runtime_shutdown_register(shutdown_ssl); + +error: + git_error_set(GIT_ERROR_NET, "could not initialize openssl: %s", + ERR_error_string(ERR_get_error(), NULL)); + SSL_CTX_free(git__ssl_ctx); + git__ssl_ctx = NULL; + return -1; +} + +/* + * When we use dynamic loading, we defer OpenSSL initialization until + * it's first used. `openssl_ensure_initialized` will do the work + * under a mutex. + */ +git_mutex openssl_mutex; +bool openssl_initialized; + +int git_openssl_stream_global_init(void) +{ +#ifndef GIT_OPENSSL_DYNAMIC + return openssl_init(); +#else + if (git_mutex_init(&openssl_mutex) != 0) + return -1; + + return 0; +#endif +} + +static int openssl_ensure_initialized(void) +{ +#ifdef GIT_OPENSSL_DYNAMIC + int error = 0; + + if (git_mutex_lock(&openssl_mutex) != 0) + return -1; + + if (!openssl_initialized) { + if ((error = git_openssl_stream_dynamic_init()) == 0) + error = openssl_init(); + + openssl_initialized = !error; + } + + error |= git_mutex_unlock(&openssl_mutex); + return error; + +#else + return 0; +#endif +} + +#if !defined(GIT_OPENSSL_LEGACY) && !defined(GIT_OPENSSL_DYNAMIC) +int git_openssl_set_locking(void) +{ +# ifdef GIT_THREADS + return 0; +# else + git_error_set(GIT_ERROR_THREAD, "libgit2 was not built with threads"); + return -1; +# endif +} +#endif + + +static int bio_create(BIO *b) +{ + BIO_set_init(b, 1); + BIO_set_data(b, NULL); + + return 1; +} + +static int bio_destroy(BIO *b) +{ + if (!b) + return 0; + + BIO_set_data(b, NULL); + + return 1; +} + +static int bio_read(BIO *b, char *buf, int len) +{ + git_stream *io = (git_stream *) BIO_get_data(b); + + return (int) git_stream_read(io, buf, len); +} + +static int bio_write(BIO *b, const char *buf, int len) +{ + git_stream *io = (git_stream *) BIO_get_data(b); + return (int) git_stream_write(io, buf, len, 0); +} + +static long bio_ctrl(BIO *b, int cmd, long num, void *ptr) +{ + GIT_UNUSED(b); + GIT_UNUSED(num); + GIT_UNUSED(ptr); + + if (cmd == BIO_CTRL_FLUSH) + return 1; + + return 0; +} + +static int bio_gets(BIO *b, char *buf, int len) +{ + GIT_UNUSED(b); + GIT_UNUSED(buf); + GIT_UNUSED(len); + return -1; +} + +static int bio_puts(BIO *b, const char *str) +{ + return bio_write(b, str, strlen(str)); +} + +static int init_bio_method(void) +{ + /* Set up the BIO_METHOD we use for wrapping our own stream implementations */ + git_stream_bio_method = BIO_meth_new(BIO_TYPE_SOURCE_SINK | BIO_get_new_index(), "git_stream"); + GIT_ERROR_CHECK_ALLOC(git_stream_bio_method); + + BIO_meth_set_write(git_stream_bio_method, bio_write); + BIO_meth_set_read(git_stream_bio_method, bio_read); + BIO_meth_set_puts(git_stream_bio_method, bio_puts); + BIO_meth_set_gets(git_stream_bio_method, bio_gets); + BIO_meth_set_ctrl(git_stream_bio_method, bio_ctrl); + BIO_meth_set_create(git_stream_bio_method, bio_create); + BIO_meth_set_destroy(git_stream_bio_method, bio_destroy); + + return 0; +} + +static int ssl_set_error(SSL *ssl, int error) +{ + int err; + unsigned long e; + + err = SSL_get_error(ssl, error); + + GIT_ASSERT(err != SSL_ERROR_WANT_READ); + GIT_ASSERT(err != SSL_ERROR_WANT_WRITE); + + switch (err) { + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + git_error_set(GIT_ERROR_SSL, "SSL error: connection failure"); + break; + case SSL_ERROR_WANT_X509_LOOKUP: + git_error_set(GIT_ERROR_SSL, "SSL error: x509 error"); + break; + case SSL_ERROR_SYSCALL: + e = ERR_get_error(); + if (e > 0) { + char errmsg[256]; + ERR_error_string_n(e, errmsg, sizeof(errmsg)); + git_error_set(GIT_ERROR_NET, "SSL error: %s", errmsg); + break; + } else if (error < 0) { + git_error_set(GIT_ERROR_OS, "SSL error: syscall failure"); + break; + } + git_error_set(GIT_ERROR_SSL, "SSL error: received early EOF"); + return GIT_EEOF; + break; + case SSL_ERROR_SSL: + { + char errmsg[256]; + e = ERR_get_error(); + ERR_error_string_n(e, errmsg, sizeof(errmsg)); + git_error_set(GIT_ERROR_SSL, "SSL error: %s", errmsg); + break; + } + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + default: + git_error_set(GIT_ERROR_SSL, "SSL error: unknown error"); + break; + } + return -1; +} + +static int ssl_teardown(SSL *ssl) +{ + int ret; + + ret = SSL_shutdown(ssl); + if (ret < 0) + ret = ssl_set_error(ssl, ret); + else + ret = 0; + + return ret; +} + +static bool check_host_name(const char *host, const char *name) +{ + return !strcasecmp(host, name) || + git_net_hostname_matches_cert(host, name); +} + +static int verify_server_cert(SSL *ssl, const char *host) +{ + X509 *cert = NULL; + X509_NAME *peer_name; + ASN1_STRING *str; + unsigned char *peer_cn = NULL; + int matched = -1, type = GEN_DNS; + GENERAL_NAMES *alts; + struct in6_addr addr6; + struct in_addr addr4; + void *addr = NULL; + int i = -1, j, error = 0; + + if (SSL_get_verify_result(ssl) != X509_V_OK) { + git_error_set(GIT_ERROR_SSL, "the SSL certificate is invalid"); + return GIT_ECERTIFICATE; + } + + /* Try to parse the host as an IP address to see if it is */ + if (p_inet_pton(AF_INET, host, &addr4)) { + type = GEN_IPADD; + addr = &addr4; + } else { + if (p_inet_pton(AF_INET6, host, &addr6)) { + type = GEN_IPADD; + addr = &addr6; + } + } + + + cert = SSL_get_peer_certificate(ssl); + if (!cert) { + error = -1; + git_error_set(GIT_ERROR_SSL, "the server did not provide a certificate"); + goto cleanup; + } + + /* Check the alternative names */ + alts = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL); + if (alts) { + int num; + + num = sk_GENERAL_NAME_num(alts); + for (i = 0; i < num && matched != 1; i++) { + const GENERAL_NAME *gn = sk_GENERAL_NAME_value(alts, i); + const char *name = (char *) ASN1_STRING_get0_data(gn->d.ia5); + size_t namelen = (size_t) ASN1_STRING_length(gn->d.ia5); + + /* Skip any names of a type we're not looking for */ + if (gn->type != type) + continue; + + if (type == GEN_DNS) { + /* If it contains embedded NULs, don't even try */ + if (memchr(name, '\0', namelen)) + continue; + + matched = !!check_host_name(host, name); + } else if (type == GEN_IPADD) { + /* Here name isn't so much a name but a binary representation of the IP */ + matched = addr && !!memcmp(name, addr, namelen); + } + } + } + GENERAL_NAMES_free(alts); + + if (matched == 0) + goto cert_fail_name; + + if (matched == 1) { + goto cleanup; + } + + /* If no alternative names are available, check the common name */ + peer_name = X509_get_subject_name(cert); + if (peer_name == NULL) + goto on_error; + + if (peer_name) { + /* Get the index of the last CN entry */ + while ((j = X509_NAME_get_index_by_NID(peer_name, NID_commonName, i)) >= 0) + i = j; + } + + if (i < 0) + goto on_error; + + str = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(peer_name, i)); + if (str == NULL) + goto on_error; + + /* Work around a bug in OpenSSL whereby ASN1_STRING_to_UTF8 fails if it's already in utf-8 */ + if (ASN1_STRING_type(str) == V_ASN1_UTF8STRING) { + int size = ASN1_STRING_length(str); + + if (size > 0) { + peer_cn = OPENSSL_malloc(size + 1); + GIT_ERROR_CHECK_ALLOC(peer_cn); + memcpy(peer_cn, ASN1_STRING_get0_data(str), size); + peer_cn[size] = '\0'; + } else { + goto cert_fail_name; + } + } else { + int size = ASN1_STRING_to_UTF8(&peer_cn, str); + GIT_ERROR_CHECK_ALLOC(peer_cn); + if (memchr(peer_cn, '\0', size)) + goto cert_fail_name; + } + + if (!check_host_name(host, (char *)peer_cn)) + goto cert_fail_name; + + goto cleanup; + +cert_fail_name: + error = GIT_ECERTIFICATE; + git_error_set(GIT_ERROR_SSL, "hostname does not match certificate"); + goto cleanup; + +on_error: + error = ssl_set_error(ssl, 0); + goto cleanup; + +cleanup: + X509_free(cert); + OPENSSL_free(peer_cn); + return error; +} + +typedef struct { + git_stream parent; + git_stream *io; + int owned; + bool connected; + char *host; + SSL *ssl; + git_cert_x509 cert_info; +} openssl_stream; + +static int openssl_connect(git_stream *stream) +{ + int ret; + BIO *bio; + openssl_stream *st = (openssl_stream *) stream; + + if (st->owned && (ret = git_stream_connect(st->io)) < 0) + return ret; + + bio = BIO_new(git_stream_bio_method); + GIT_ERROR_CHECK_ALLOC(bio); + + BIO_set_data(bio, st->io); + SSL_set_bio(st->ssl, bio, bio); + + /* specify the host in case SNI is needed */ +#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME + SSL_set_tlsext_host_name(st->ssl, st->host); +#endif + + if ((ret = SSL_connect(st->ssl)) <= 0) + return ssl_set_error(st->ssl, ret); + + st->connected = true; + + return verify_server_cert(st->ssl, st->host); +} + +static int openssl_certificate(git_cert **out, git_stream *stream) +{ + openssl_stream *st = (openssl_stream *) stream; + X509 *cert = SSL_get_peer_certificate(st->ssl); + unsigned char *guard, *encoded_cert = NULL; + int error, len; + + /* Retrieve the length of the certificate first */ + len = i2d_X509(cert, NULL); + if (len < 0) { + git_error_set(GIT_ERROR_NET, "failed to retrieve certificate information"); + error = -1; + goto out; + } + + encoded_cert = git__malloc(len); + GIT_ERROR_CHECK_ALLOC(encoded_cert); + /* i2d_X509 makes 'guard' point to just after the data */ + guard = encoded_cert; + + len = i2d_X509(cert, &guard); + if (len < 0) { + git_error_set(GIT_ERROR_NET, "failed to retrieve certificate information"); + error = -1; + goto out; + } + + st->cert_info.parent.cert_type = GIT_CERT_X509; + st->cert_info.data = encoded_cert; + st->cert_info.len = len; + encoded_cert = NULL; + + *out = &st->cert_info.parent; + error = 0; + +out: + git__free(encoded_cert); + X509_free(cert); + return error; +} + +static int openssl_set_proxy(git_stream *stream, const git_proxy_options *proxy_opts) +{ + openssl_stream *st = (openssl_stream *) stream; + + return git_stream_set_proxy(st->io, proxy_opts); +} + +static ssize_t openssl_write(git_stream *stream, const char *data, size_t data_len, int flags) +{ + openssl_stream *st = (openssl_stream *) stream; + int ret, len = min(data_len, INT_MAX); + + GIT_UNUSED(flags); + + if ((ret = SSL_write(st->ssl, data, len)) <= 0) + return ssl_set_error(st->ssl, ret); + + return ret; +} + +static ssize_t openssl_read(git_stream *stream, void *data, size_t len) +{ + openssl_stream *st = (openssl_stream *) stream; + int ret; + + if ((ret = SSL_read(st->ssl, data, len)) <= 0) + return ssl_set_error(st->ssl, ret); + + return ret; +} + +static int openssl_close(git_stream *stream) +{ + openssl_stream *st = (openssl_stream *) stream; + int ret; + + if (st->connected && (ret = ssl_teardown(st->ssl)) < 0) + return -1; + + st->connected = false; + + return st->owned ? git_stream_close(st->io) : 0; +} + +static void openssl_free(git_stream *stream) +{ + openssl_stream *st = (openssl_stream *) stream; + + if (st->owned) + git_stream_free(st->io); + + SSL_free(st->ssl); + git__free(st->host); + git__free(st->cert_info.data); + git__free(st); +} + +static int openssl_stream_wrap( + git_stream **out, + git_stream *in, + const char *host, + int owned) +{ + openssl_stream *st; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(in); + GIT_ASSERT_ARG(host); + + st = git__calloc(1, sizeof(openssl_stream)); + GIT_ERROR_CHECK_ALLOC(st); + + st->io = in; + st->owned = owned; + + st->ssl = SSL_new(git__ssl_ctx); + if (st->ssl == NULL) { + git_error_set(GIT_ERROR_SSL, "failed to create ssl object"); + git__free(st); + return -1; + } + + st->host = git__strdup(host); + GIT_ERROR_CHECK_ALLOC(st->host); + + st->parent.version = GIT_STREAM_VERSION; + st->parent.encrypted = 1; + st->parent.proxy_support = git_stream_supports_proxy(st->io); + st->parent.connect = openssl_connect; + st->parent.certificate = openssl_certificate; + st->parent.set_proxy = openssl_set_proxy; + st->parent.read = openssl_read; + st->parent.write = openssl_write; + st->parent.close = openssl_close; + st->parent.free = openssl_free; + + *out = (git_stream *) st; + return 0; +} + +int git_openssl_stream_wrap(git_stream **out, git_stream *in, const char *host) +{ + if (openssl_ensure_initialized() < 0) + return -1; + + return openssl_stream_wrap(out, in, host, 0); +} + +int git_openssl_stream_new(git_stream **out, const char *host, const char *port) +{ + git_stream *stream = NULL; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(host); + GIT_ASSERT_ARG(port); + + if (openssl_ensure_initialized() < 0) + return -1; + + if ((error = git_socket_stream_new(&stream, host, port)) < 0) + return error; + + if ((error = openssl_stream_wrap(out, stream, host, 1)) < 0) { + git_stream_close(stream); + git_stream_free(stream); + } + + return error; +} + +int git_openssl__set_cert_location(const char *file, const char *path) +{ + if (openssl_ensure_initialized() < 0) + return -1; + + if (SSL_CTX_load_verify_locations(git__ssl_ctx, file, path) == 0) { + char errmsg[256]; + + ERR_error_string_n(ERR_get_error(), errmsg, sizeof(errmsg)); + git_error_set(GIT_ERROR_SSL, "OpenSSL error: failed to load certificates: %s", + errmsg); + + return -1; + } + return 0; +} + +#else + +#include "stream.h" +#include "git2/sys/openssl.h" + +int git_openssl_stream_global_init(void) +{ + return 0; +} + +int git_openssl_set_locking(void) +{ + git_error_set(GIT_ERROR_SSL, "libgit2 was not built with OpenSSL support"); + return -1; +} + +#endif diff --git a/src/libgit2/streams/openssl.h b/src/libgit2/streams/openssl.h new file mode 100644 index 0000000..89fb60a --- /dev/null +++ b/src/libgit2/streams/openssl.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_streams_openssl_h__ +#define INCLUDE_streams_openssl_h__ + +#include "common.h" +#include "streams/openssl_legacy.h" +#include "streams/openssl_dynamic.h" + +#include "git2/sys/stream.h" + +extern int git_openssl_stream_global_init(void); + +#if defined(GIT_OPENSSL) && !defined(GIT_OPENSSL_DYNAMIC) +# include +# include +# include +# include +# endif + +#ifdef GIT_OPENSSL +extern int git_openssl__set_cert_location(const char *file, const char *path); +extern int git_openssl_stream_new(git_stream **out, const char *host, const char *port); +extern int git_openssl_stream_wrap(git_stream **out, git_stream *in, const char *host); +#endif + +#endif diff --git a/src/libgit2/streams/openssl_dynamic.c b/src/libgit2/streams/openssl_dynamic.c new file mode 100644 index 0000000..222c109 --- /dev/null +++ b/src/libgit2/streams/openssl_dynamic.c @@ -0,0 +1,313 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "streams/openssl.h" +#include "streams/openssl_dynamic.h" + +#if defined(GIT_OPENSSL) && defined(GIT_OPENSSL_DYNAMIC) + +#include "runtime.h" + +#include + +unsigned char *(*ASN1_STRING_data)(ASN1_STRING *x); +const unsigned char *(*ASN1_STRING_get0_data)(const ASN1_STRING *x); +int (*ASN1_STRING_length)(const ASN1_STRING *x); +int (*ASN1_STRING_to_UTF8)(unsigned char **out, const ASN1_STRING *in); +int (*ASN1_STRING_type)(const ASN1_STRING *x); + +void *(*BIO_get_data)(BIO *a); +int (*BIO_get_new_index)(void); +int (*OPENSSL_init_ssl)(uint64_t opts, const void *settings); +void (*BIO_meth_free)(BIO_METHOD *biom); +int (*BIO_meth_set_create)(BIO_METHOD *biom, int (*create) (BIO *)); +int (*BIO_meth_set_ctrl)(BIO_METHOD *biom, long (*ctrl) (BIO *, int, long, void *)); +int (*BIO_meth_set_destroy)(BIO_METHOD *biom, int (*destroy) (BIO *)); +int (*BIO_meth_set_gets)(BIO_METHOD *biom, int (*gets) (BIO *, char *, int)); +int (*BIO_meth_set_puts)(BIO_METHOD *biom, int (*puts) (BIO *, const char *)); +int (*BIO_meth_set_read)(BIO_METHOD *biom, int (*read) (BIO *, char *, int)); +int (*BIO_meth_set_write)(BIO_METHOD *biom, int (*write) (BIO *, const char *, int)); +BIO_METHOD *(*BIO_meth_new)(int type, const char *name); +BIO *(*BIO_new)(const BIO_METHOD *type); +void (*BIO_set_data)(BIO *a, void *ptr); +void (*BIO_set_init)(BIO *a, int init); + +void (*CRYPTO_free)(void *ptr, const char *file, int line); +void *(*CRYPTO_malloc)(size_t num, const char *file, int line); +int (*CRYPTO_num_locks)(void); +void (*CRYPTO_set_locking_callback)(void (*func)(int mode, int type, const char *file, int line)); +int (*CRYPTO_set_mem_functions)(void *(*m)(size_t bytes), void *(*r)(void *mem, size_t size), void (*f)(void *mem)); +int (*CRYPTO_THREADID_set_callback)(void (*func)(CRYPTO_THREADID *id)); +void (*CRYPTO_THREADID_set_numeric)(CRYPTO_THREADID *id, unsigned long val); + +char *(*ERR_error_string)(unsigned long e, char *buf); +void (*ERR_error_string_n)(unsigned long e, char *buf, size_t len); +unsigned long (*ERR_get_error)(void); + +int (*SSL_connect)(SSL *ssl); +long (*SSL_ctrl)(SSL *ssl, int cmd, long arg, void *parg); +void (*SSL_free)(SSL *ssl); +int (*SSL_get_error)(SSL *ssl, int ret); +X509 *(*SSL_get_peer_certificate)(const SSL *ssl); +long (*SSL_get_verify_result)(const SSL *ssl); +int (*SSL_library_init)(void); +void (*SSL_load_error_strings)(void); +SSL *(*SSL_new)(SSL_CTX *ctx); +int (*SSL_read)(SSL *ssl, const void *buf, int num); +void (*SSL_set_bio)(SSL *ssl, BIO *rbio, BIO *wbio); +int (*SSL_shutdown)(SSL *ssl); +int (*SSL_write)(SSL *ssl, const void *buf, int num); + +long (*SSL_CTX_ctrl)(SSL_CTX *ctx, int cmd, long larg, void *parg); +void (*SSL_CTX_free)(SSL_CTX *ctx); +SSL_CTX *(*SSL_CTX_new)(const SSL_METHOD *method); +int (*SSL_CTX_set_cipher_list)(SSL_CTX *ctx, const char *str); +int (*SSL_CTX_set_default_verify_paths)(SSL_CTX *ctx); +long (*SSL_CTX_set_options)(SSL_CTX *ctx, long options); +void (*SSL_CTX_set_verify)(SSL_CTX *ctx, int mode, int (*verify_callback)(int, X509_STORE_CTX *)); +int (*SSL_CTX_load_verify_locations)(SSL_CTX *ctx, const char *CAfile, const char *CApath); + +const SSL_METHOD *(*SSLv23_method)(void); +const SSL_METHOD *(*TLS_method)(void); + +ASN1_STRING *(*X509_NAME_ENTRY_get_data)(const X509_NAME_ENTRY *ne); +X509_NAME_ENTRY *(*X509_NAME_get_entry)(X509_NAME *name, int loc); +int (*X509_NAME_get_index_by_NID)(X509_NAME *name, int nid, int lastpos); +void (*X509_free)(X509 *a); +void *(*X509_get_ext_d2i)(const X509 *x, int nid, int *crit, int *idx); +X509_NAME *(*X509_get_subject_name)(const X509 *x); + +int (*i2d_X509)(X509 *a, unsigned char **ppout); + +int (*OPENSSL_sk_num)(const void *sk); +void *(*OPENSSL_sk_value)(const void *sk, int i); +void (*OPENSSL_sk_free)(void *sk); + +int (*sk_num)(const void *sk); +void *(*sk_value)(const void *sk, int i); +void (*sk_free)(void *sk); + +static void *openssl_handle; + +GIT_INLINE(void *) openssl_sym(int *err, const char *name, bool required) +{ + void *symbol; + + /* if we've seen an err, noop to retain it */ + if (*err) + return NULL; + + + if ((symbol = dlsym(openssl_handle, name)) == NULL && required) { + const char *msg = dlerror(); + git_error_set(GIT_ERROR_SSL, "could not load ssl function '%s': %s", name, msg ? msg : "unknown error"); + *err = -1; + } + + return symbol; +} + +static void dynamic_shutdown(void) +{ + dlclose(openssl_handle); + openssl_handle = NULL; +} + +int git_openssl_stream_dynamic_init(void) +{ + int err = 0; + + if ((openssl_handle = dlopen("libssl.so.1.1", RTLD_NOW)) == NULL && + (openssl_handle = dlopen("libssl.1.1.dylib", RTLD_NOW)) == NULL && + (openssl_handle = dlopen("libssl.so.1.0.0", RTLD_NOW)) == NULL && + (openssl_handle = dlopen("libssl.1.0.0.dylib", RTLD_NOW)) == NULL && + (openssl_handle = dlopen("libssl.so.10", RTLD_NOW)) == NULL && + (openssl_handle = dlopen("libssl.so.3", RTLD_NOW)) == NULL) { + git_error_set(GIT_ERROR_SSL, "could not load ssl libraries"); + return -1; + } + + ASN1_STRING_data = (unsigned char *(*)(ASN1_STRING *x))openssl_sym(&err, "ASN1_STRING_data", false); + ASN1_STRING_get0_data = (const unsigned char *(*)(const ASN1_STRING *x))openssl_sym(&err, "ASN1_STRING_get0_data", false); + ASN1_STRING_length = (int (*)(const ASN1_STRING *))openssl_sym(&err, "ASN1_STRING_length", true); + ASN1_STRING_to_UTF8 = (int (*)(unsigned char **, const ASN1_STRING *))openssl_sym(&err, "ASN1_STRING_to_UTF8", true); + ASN1_STRING_type = (int (*)(const ASN1_STRING *))openssl_sym(&err, "ASN1_STRING_type", true); + + BIO_get_data = (void *(*)(BIO *))openssl_sym(&err, "BIO_get_data", false); + BIO_get_new_index = (int (*)(void))openssl_sym(&err, "BIO_get_new_index", false); + BIO_meth_free = (void (*)(BIO_METHOD *))openssl_sym(&err, "BIO_meth_free", false); + BIO_meth_new = (BIO_METHOD *(*)(int, const char *))openssl_sym(&err, "BIO_meth_new", false); + BIO_meth_set_create = (int (*)(BIO_METHOD *, int (*)(BIO *)))openssl_sym(&err, "BIO_meth_set_create", false); + BIO_meth_set_ctrl = (int (*)(BIO_METHOD *, long (*)(BIO *, int, long, void *)))openssl_sym(&err, "BIO_meth_set_ctrl", false); + BIO_meth_set_destroy = (int (*)(BIO_METHOD *, int (*)(BIO *)))openssl_sym(&err, "BIO_meth_set_destroy", false); + BIO_meth_set_gets = (int (*)(BIO_METHOD *, int (*)(BIO *, char *, int)))openssl_sym(&err, "BIO_meth_set_gets", false); + BIO_meth_set_puts = (int (*)(BIO_METHOD *, int (*)(BIO *, const char *)))openssl_sym(&err, "BIO_meth_set_puts", false); + BIO_meth_set_read = (int (*)(BIO_METHOD *, int (*)(BIO *, char *, int)))openssl_sym(&err, "BIO_meth_set_read", false); + BIO_meth_set_write = (int (*)(BIO_METHOD *, int (*)(BIO *, const char *, int)))openssl_sym(&err, "BIO_meth_set_write", false); + BIO_new = (BIO *(*)(const BIO_METHOD *))openssl_sym(&err, "BIO_new", true); + BIO_set_data = (void (*)(BIO *a, void *))openssl_sym(&err, "BIO_set_data", false); + BIO_set_init = (void (*)(BIO *a, int))openssl_sym(&err, "BIO_set_init", false); + + CRYPTO_free = (void (*)(void *, const char *, int))openssl_sym(&err, "CRYPTO_free", true); + CRYPTO_malloc = (void *(*)(size_t, const char *, int))openssl_sym(&err, "CRYPTO_malloc", true); + CRYPTO_num_locks = (int (*)(void))openssl_sym(&err, "CRYPTO_num_locks", false); + CRYPTO_set_locking_callback = (void (*)(void (*)(int, int, const char *, int)))openssl_sym(&err, "CRYPTO_set_locking_callback", false); + CRYPTO_set_mem_functions = (int (*)(void *(*)(size_t), void *(*)(void *, size_t), void (*f)(void *)))openssl_sym(&err, "CRYPTO_set_mem_functions", true); + + CRYPTO_THREADID_set_callback = (int (*)(void (*)(CRYPTO_THREADID *)))openssl_sym(&err, "CRYPTO_THREADID_set_callback", false); + CRYPTO_THREADID_set_numeric = (void (*)(CRYPTO_THREADID *, unsigned long))openssl_sym(&err, "CRYPTO_THREADID_set_numeric", false); + + ERR_error_string = (char *(*)(unsigned long, char *))openssl_sym(&err, "ERR_error_string", true); + ERR_error_string_n = (void (*)(unsigned long, char *, size_t))openssl_sym(&err, "ERR_error_string_n", true); + ERR_get_error = (unsigned long (*)(void))openssl_sym(&err, "ERR_get_error", true); + + OPENSSL_init_ssl = (int (*)(uint64_t opts, const void *settings))openssl_sym(&err, "OPENSSL_init_ssl", false); + OPENSSL_sk_num = (int (*)(const void *))openssl_sym(&err, "OPENSSL_sk_num", false); + OPENSSL_sk_value = (void *(*)(const void *sk, int i))openssl_sym(&err, "OPENSSL_sk_value", false); + OPENSSL_sk_free = (void (*)(void *))openssl_sym(&err, "OPENSSL_sk_free", false); + + sk_num = (int (*)(const void *))openssl_sym(&err, "sk_num", false); + sk_value = (void *(*)(const void *sk, int i))openssl_sym(&err, "sk_value", false); + sk_free = (void (*)(void *))openssl_sym(&err, "sk_free", false); + + SSL_connect = (int (*)(SSL *))openssl_sym(&err, "SSL_connect", true); + SSL_ctrl = (long (*)(SSL *, int, long, void *))openssl_sym(&err, "SSL_ctrl", true); + SSL_library_init = (int (*)(void))openssl_sym(&err, "SSL_library_init", false); + SSL_free = (void (*)(SSL *))openssl_sym(&err, "SSL_free", true); + SSL_get_error = (int (*)(SSL *, int))openssl_sym(&err, "SSL_get_error", true); + SSL_get_verify_result = (long (*)(const SSL *ssl))openssl_sym(&err, "SSL_get_verify_result", true); + SSL_load_error_strings = (void (*)(void))openssl_sym(&err, "SSL_load_error_strings", false); + SSL_new = (SSL *(*)(SSL_CTX *))openssl_sym(&err, "SSL_new", true); + SSL_read = (int (*)(SSL *, const void *, int))openssl_sym(&err, "SSL_read", true); + SSL_set_bio = (void (*)(SSL *, BIO *, BIO *))openssl_sym(&err, "SSL_set_bio", true); + SSL_shutdown = (int (*)(SSL *ssl))openssl_sym(&err, "SSL_shutdown", true); + SSL_write = (int (*)(SSL *, const void *, int))openssl_sym(&err, "SSL_write", true); + + if (!(SSL_get_peer_certificate = (X509 *(*)(const SSL *))openssl_sym(&err, "SSL_get_peer_certificate", false))) { + SSL_get_peer_certificate = (X509 *(*)(const SSL *))openssl_sym(&err, "SSL_get1_peer_certificate", true); + } + + SSL_CTX_ctrl = (long (*)(SSL_CTX *, int, long, void *))openssl_sym(&err, "SSL_CTX_ctrl", true); + SSL_CTX_free = (void (*)(SSL_CTX *))openssl_sym(&err, "SSL_CTX_free", true); + SSL_CTX_new = (SSL_CTX *(*)(const SSL_METHOD *))openssl_sym(&err, "SSL_CTX_new", true); + SSL_CTX_set_cipher_list = (int (*)(SSL_CTX *, const char *))openssl_sym(&err, "SSL_CTX_set_cipher_list", true); + SSL_CTX_set_default_verify_paths = (int (*)(SSL_CTX *ctx))openssl_sym(&err, "SSL_CTX_set_default_verify_paths", true); + SSL_CTX_set_options = (long (*)(SSL_CTX *, long))openssl_sym(&err, "SSL_CTX_set_options", false); + SSL_CTX_set_verify = (void (*)(SSL_CTX *, int, int (*)(int, X509_STORE_CTX *)))openssl_sym(&err, "SSL_CTX_set_verify", true); + SSL_CTX_load_verify_locations = (int (*)(SSL_CTX *, const char *, const char *))openssl_sym(&err, "SSL_CTX_load_verify_locations", true); + + SSLv23_method = (const SSL_METHOD *(*)(void))openssl_sym(&err, "SSLv23_method", false); + TLS_method = (const SSL_METHOD *(*)(void))openssl_sym(&err, "TLS_method", false); + + X509_NAME_ENTRY_get_data = (ASN1_STRING *(*)(const X509_NAME_ENTRY *))openssl_sym(&err, "X509_NAME_ENTRY_get_data", true); + X509_NAME_get_entry = (X509_NAME_ENTRY *(*)(X509_NAME *, int))openssl_sym(&err, "X509_NAME_get_entry", true); + X509_NAME_get_index_by_NID = (int (*)(X509_NAME *, int, int))openssl_sym(&err, "X509_NAME_get_index_by_NID", true); + X509_free = (void (*)(X509 *))openssl_sym(&err, "X509_free", true); + X509_get_ext_d2i = (void *(*)(const X509 *x, int nid, int *crit, int *idx))openssl_sym(&err, "X509_get_ext_d2i", true); + X509_get_subject_name = (X509_NAME *(*)(const X509 *))openssl_sym(&err, "X509_get_subject_name", true); + + i2d_X509 = (int (*)(X509 *a, unsigned char **ppout))openssl_sym(&err, "i2d_X509", true); + + if (err) + goto on_error; + + /* Add legacy functionality */ + if (!OPENSSL_init_ssl) { + OPENSSL_init_ssl = OPENSSL_init_ssl__legacy; + + if (!SSL_library_init || + !SSL_load_error_strings || + !CRYPTO_num_locks || + !CRYPTO_set_locking_callback || + !CRYPTO_THREADID_set_callback || + !CRYPTO_THREADID_set_numeric) { + git_error_set(GIT_ERROR_SSL, "could not load legacy openssl initialization functions"); + goto on_error; + } + } + + if (!SSL_CTX_set_options) + SSL_CTX_set_options = SSL_CTX_set_options__legacy; + + if (TLS_method) + SSLv23_method = TLS_method; + + if (!BIO_meth_new) { + BIO_meth_new = BIO_meth_new__legacy; + BIO_meth_new = BIO_meth_new__legacy; + BIO_meth_free = BIO_meth_free__legacy; + BIO_meth_set_write = BIO_meth_set_write__legacy; + BIO_meth_set_read = BIO_meth_set_read__legacy; + BIO_meth_set_puts = BIO_meth_set_puts__legacy; + BIO_meth_set_gets = BIO_meth_set_gets__legacy; + BIO_meth_set_ctrl = BIO_meth_set_ctrl__legacy; + BIO_meth_set_create = BIO_meth_set_create__legacy; + BIO_meth_set_destroy = BIO_meth_set_destroy__legacy; + BIO_get_new_index = BIO_get_new_index__legacy; + BIO_set_data = BIO_set_data__legacy; + BIO_set_init = BIO_set_init__legacy; + BIO_get_data = BIO_get_data__legacy; + } + + if (!ASN1_STRING_get0_data) { + if (!ASN1_STRING_data) { + git_error_set(GIT_ERROR_SSL, "could not load legacy openssl string function"); + goto on_error; + } + + ASN1_STRING_get0_data = ASN1_STRING_get0_data__legacy; + } + + if ((!OPENSSL_sk_num && !sk_num) || + (!OPENSSL_sk_value && !sk_value) || + (!OPENSSL_sk_free && !sk_free)) { + git_error_set(GIT_ERROR_SSL, "could not load legacy openssl stack functions"); + goto on_error; + } + + if (git_runtime_shutdown_register(dynamic_shutdown) != 0) + goto on_error; + + return 0; + +on_error: + dlclose(openssl_handle); + return -1; +} + + +int sk_GENERAL_NAME_num(const GENERAL_NAME *sk) +{ + if (OPENSSL_sk_num) + return OPENSSL_sk_num(sk); + else if (sk_num) + return sk_num(sk); + + GIT_ASSERT_WITH_RETVAL(false, 0); + return 0; +} + +GENERAL_NAME *sk_GENERAL_NAME_value(const GENERAL_NAME *sk, int i) +{ + if (OPENSSL_sk_value) + return OPENSSL_sk_value(sk, i); + else if (sk_value) + return sk_value(sk, i); + + GIT_ASSERT_WITH_RETVAL(false, NULL); + return NULL; +} + +void GENERAL_NAMES_free(GENERAL_NAME *sk) +{ + if (OPENSSL_sk_free) + OPENSSL_sk_free(sk); + else if (sk_free) + sk_free(sk); +} + +#endif /* GIT_OPENSSL && GIT_OPENSSL_DYNAMIC */ diff --git a/src/libgit2/streams/openssl_dynamic.h b/src/libgit2/streams/openssl_dynamic.h new file mode 100644 index 0000000..a996919 --- /dev/null +++ b/src/libgit2/streams/openssl_dynamic.h @@ -0,0 +1,348 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are adhered to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the routines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publicly available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ +/* ==================================================================== + * Copyright (c) 1998-2007 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ +/* ==================================================================== + * Copyright 2002 Sun Microsystems, Inc. ALL RIGHTS RESERVED. + * ECC cipher suite support in OpenSSL originally developed by + * SUN MICROSYSTEMS, INC., and contributed to the OpenSSL project. + */ +/* ==================================================================== + * Copyright 2005 Nokia. All rights reserved. + * + * The portions of the attached software ("Contribution") is developed by + * Nokia Corporation and is licensed pursuant to the OpenSSL open source + * license. + * + * The Contribution, originally written by Mika Kousa and Pasi Eronen of + * Nokia Corporation, consists of the "PSK" (Pre-Shared Key) ciphersuites + * support (see RFC 4279) to OpenSSL. + * + * No patent licenses or other rights except those expressly stated in + * the OpenSSL open source license shall be deemed granted or received + * expressly, by implication, estoppel, or otherwise. + * + * No assurances are provided by Nokia that the Contribution does not + * infringe the patent or other intellectual property rights of any third + * party or that the license provides you with all the necessary rights + * to make use of the Contribution. + * + * THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. IN + * ADDITION TO THE DISCLAIMERS INCLUDED IN THE LICENSE, NOKIA + * SPECIFICALLY DISCLAIMS ANY LIABILITY FOR CLAIMS BROUGHT BY YOU OR ANY + * OTHER ENTITY BASED ON INFRINGEMENT OF INTELLECTUAL PROPERTY RIGHTS OR + * OTHERWISE. + */ + +#ifndef INCLUDE_streams_openssl_dynamic_h__ +#define INCLUDE_streams_openssl_dynamic_h__ + +#ifdef GIT_OPENSSL_DYNAMIC + +# define BIO_CTRL_FLUSH 11 + +# define BIO_TYPE_SOURCE_SINK 0x0400 + +# define CRYPTO_LOCK 1 + +# define GEN_DNS 2 +# define GEN_IPADD 7 + +# define NID_commonName 13 +# define NID_subject_alt_name 85 + +# define SSL_VERIFY_NONE 0x00 + +# define SSL_CTRL_OPTIONS 32 +# define SSL_CTRL_MODE 33 +# define SSL_CTRL_SET_TLSEXT_HOSTNAME 55 + +# define SSL_ERROR_NONE 0 +# define SSL_ERROR_SSL 1 +# define SSL_ERROR_WANT_READ 2 +# define SSL_ERROR_WANT_WRITE 3 +# define SSL_ERROR_WANT_X509_LOOKUP 4 +# define SSL_ERROR_SYSCALL 5 +# define SSL_ERROR_ZERO_RETURN 6 +# define SSL_ERROR_WANT_CONNECT 7 +# define SSL_ERROR_WANT_ACCEPT 8 + +# define SSL_OP_NO_COMPRESSION 0x00020000L +# define SSL_OP_NO_SSLv2 0x01000000L +# define SSL_OP_NO_SSLv3 0x02000000L + +# define SSL_MODE_AUTO_RETRY 0x00000004L + +# define TLSEXT_NAMETYPE_host_name 0 + +# define V_ASN1_UTF8STRING 12 + +# define X509_V_OK 0 + +/* Most of the OpenSSL types are mercifully opaque, so we can treat them like `void *` */ +typedef struct bio_st BIO; +typedef struct bio_method_st BIO_METHOD; +typedef void bio_info_cb; +typedef void * CRYPTO_EX_DATA; +typedef void CRYPTO_THREADID; +typedef void GENERAL_NAMES; +typedef void SSL; +typedef void SSL_CTX; +typedef void SSL_METHOD; +typedef void X509; +typedef void X509_NAME; +typedef void X509_NAME_ENTRY; +typedef void X509_STORE_CTX; + +typedef struct { + int length; + int type; + unsigned char *data; + long flags; +} ASN1_STRING; + +typedef struct { + int type; + union { + char *ptr; + ASN1_STRING *ia5; + } d; +} GENERAL_NAME; + +struct bio_st { + BIO_METHOD *method; + /* bio, mode, argp, argi, argl, ret */ + long (*callback) (struct bio_st *, int, const char *, int, long, long); + char *cb_arg; /* first argument for the callback */ + int init; + int shutdown; + int flags; /* extra storage */ + int retry_reason; + int num; + void *ptr; + struct bio_st *next_bio; /* used by filter BIOs */ + struct bio_st *prev_bio; /* used by filter BIOs */ + int references; + unsigned long num_read; + unsigned long num_write; + CRYPTO_EX_DATA ex_data; +}; + +struct bio_method_st { + int type; + const char *name; + int (*bwrite) (BIO *, const char *, int); + int (*bread) (BIO *, char *, int); + int (*bputs) (BIO *, const char *); + int (*bgets) (BIO *, char *, int); + long (*ctrl) (BIO *, int, long, void *); + int (*create) (BIO *); + int (*destroy) (BIO *); + long (*callback_ctrl) (BIO *, int, bio_info_cb *); +}; + +extern unsigned char *(*ASN1_STRING_data)(ASN1_STRING *x); +extern const unsigned char *(*ASN1_STRING_get0_data)(const ASN1_STRING *x); +extern int (*ASN1_STRING_length)(const ASN1_STRING *x); +extern int (*ASN1_STRING_to_UTF8)(unsigned char **out, const ASN1_STRING *in); +extern int (*ASN1_STRING_type)(const ASN1_STRING *x); + +extern void *(*BIO_get_data)(BIO *a); +extern int (*BIO_get_new_index)(void); +extern int (*OPENSSL_init_ssl)(uint64_t opts, const void *settings); +extern void (*BIO_meth_free)(BIO_METHOD *biom); +extern int (*BIO_meth_set_create)(BIO_METHOD *biom, int (*create) (BIO *)); +extern int (*BIO_meth_set_ctrl)(BIO_METHOD *biom, long (*ctrl) (BIO *, int, long, void *)); +extern int (*BIO_meth_set_destroy)(BIO_METHOD *biom, int (*destroy) (BIO *)); +extern int (*BIO_meth_set_gets)(BIO_METHOD *biom, int (*gets) (BIO *, char *, int)); +extern int (*BIO_meth_set_puts)(BIO_METHOD *biom, int (*puts) (BIO *, const char *)); +extern int (*BIO_meth_set_read)(BIO_METHOD *biom, int (*read) (BIO *, char *, int)); +extern int (*BIO_meth_set_write)(BIO_METHOD *biom, int (*write) (BIO *, const char *, int)); +extern BIO_METHOD *(*BIO_meth_new)(int type, const char *name); +extern BIO *(*BIO_new)(const BIO_METHOD *type); +extern void (*BIO_set_data)(BIO *a, void *ptr); +extern void (*BIO_set_init)(BIO *a, int init); + +extern void (*CRYPTO_free)(void *ptr, const char *file, int line); +extern void *(*CRYPTO_malloc)(size_t num, const char *file, int line); +extern int (*CRYPTO_num_locks)(void); +extern void (*CRYPTO_set_locking_callback)(void (*func)(int mode, int type, const char *file, int line)); +extern int (*CRYPTO_set_mem_functions)(void *(*m)(size_t bytes), void *(*r)(void *mem, size_t size), void (*f)(void *mem)); +extern int (*CRYPTO_THREADID_set_callback)(void (*func)(CRYPTO_THREADID *id)); +extern void (*CRYPTO_THREADID_set_numeric)(CRYPTO_THREADID *id, unsigned long val); + +extern char *(*ERR_error_string)(unsigned long e, char *buf); +extern void (*ERR_error_string_n)(unsigned long e, char *buf, size_t len); +extern unsigned long (*ERR_get_error)(void); + +# define OPENSSL_malloc(num) CRYPTO_malloc(num, __FILE__, __LINE__) +# define OPENSSL_free(addr) CRYPTO_free(addr, __FILE__, __LINE__) + +extern int (*SSL_connect)(SSL *ssl); +extern long (*SSL_ctrl)(SSL *ssl, int cmd, long arg, void *parg); +extern void (*SSL_free)(SSL *ssl); +extern int (*SSL_get_error)(SSL *ssl, int ret); +extern X509 *(*SSL_get_peer_certificate)(const SSL *ssl); +extern long (*SSL_get_verify_result)(const SSL *ssl); +extern int (*SSL_library_init)(void); +extern void (*SSL_load_error_strings)(void); +extern SSL *(*SSL_new)(SSL_CTX *ctx); +extern int (*SSL_read)(SSL *ssl, const void *buf, int num); +extern void (*SSL_set_bio)(SSL *ssl, BIO *rbio, BIO *wbio); +extern int (*SSL_shutdown)(SSL *ssl); +extern int (*SSL_write)(SSL *ssl, const void *buf, int num); + +# define SSL_set_tlsext_host_name(s, name) SSL_ctrl((s), SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, (char *)(name)); + +extern long (*SSL_CTX_ctrl)(SSL_CTX *ctx, int cmd, long larg, void *parg); +extern void (*SSL_CTX_free)(SSL_CTX *ctx); +extern SSL_CTX *(*SSL_CTX_new)(const SSL_METHOD *method); +extern int (*SSL_CTX_set_cipher_list)(SSL_CTX *ctx, const char *str); +extern int (*SSL_CTX_set_default_verify_paths)(SSL_CTX *ctx); +extern long (*SSL_CTX_set_options)(SSL_CTX *ctx, long options); +extern void (*SSL_CTX_set_verify)(SSL_CTX *ctx, int mode, int (*verify_callback)(int, X509_STORE_CTX *)); +extern int (*SSL_CTX_load_verify_locations)(SSL_CTX *ctx, const char *CAfile, const char *CApath); + +# define SSL_CTX_set_mode(ctx, mode) SSL_CTX_ctrl((ctx), SSL_CTRL_MODE, (mode), NULL); + +extern const SSL_METHOD *(*SSLv23_method)(void); +extern const SSL_METHOD *(*TLS_method)(void); + +extern ASN1_STRING *(*X509_NAME_ENTRY_get_data)(const X509_NAME_ENTRY *ne); +extern X509_NAME_ENTRY *(*X509_NAME_get_entry)(X509_NAME *name, int loc); +extern int (*X509_NAME_get_index_by_NID)(X509_NAME *name, int nid, int lastpos); +extern void (*X509_free)(X509 *a); +extern void *(*X509_get_ext_d2i)(const X509 *x, int nid, int *crit, int *idx); +extern X509_NAME *(*X509_get_subject_name)(const X509 *x); + +extern int (*i2d_X509)(X509 *a, unsigned char **ppout); + +extern int (*OPENSSL_sk_num)(const void *sk); +extern void *(*OPENSSL_sk_value)(const void *sk, int i); +extern void (*OPENSSL_sk_free)(void *sk); + +extern int (*sk_num)(const void *sk); +extern void *(*sk_value)(const void *sk, int i); +extern void (*sk_free)(void *sk); + +extern int sk_GENERAL_NAME_num(const GENERAL_NAME *sk); +extern GENERAL_NAME *sk_GENERAL_NAME_value(const GENERAL_NAME *sk, int i); +extern void GENERAL_NAMES_free(GENERAL_NAME *sk); + +extern int git_openssl_stream_dynamic_init(void); + +#endif /* GIT_OPENSSL_DYNAMIC */ + +#endif diff --git a/src/libgit2/streams/openssl_legacy.c b/src/libgit2/streams/openssl_legacy.c new file mode 100644 index 0000000..e61e6ef --- /dev/null +++ b/src/libgit2/streams/openssl_legacy.c @@ -0,0 +1,203 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "streams/openssl.h" +#include "streams/openssl_legacy.h" + +#include "runtime.h" +#include "git2/sys/openssl.h" + +#if defined(GIT_OPENSSL) && !defined(GIT_OPENSSL_DYNAMIC) +# include +# include +# include +# include +#endif + +#if defined(GIT_OPENSSL_LEGACY) || defined(GIT_OPENSSL_DYNAMIC) + +/* + * OpenSSL 1.1 made BIO opaque so we have to use functions to interact with it + * which do not exist in previous versions. We define these inline functions so + * we can program against the interface instead of littering the implementation + * with ifdefs. We do the same for OPENSSL_init_ssl. + */ + +int OPENSSL_init_ssl__legacy(uint64_t opts, const void *settings) +{ + GIT_UNUSED(opts); + GIT_UNUSED(settings); + SSL_load_error_strings(); + SSL_library_init(); + return 0; +} + +BIO_METHOD *BIO_meth_new__legacy(int type, const char *name) +{ + BIO_METHOD *meth = git__calloc(1, sizeof(BIO_METHOD)); + if (!meth) { + return NULL; + } + + meth->type = type; + meth->name = name; + + return meth; +} + +void BIO_meth_free__legacy(BIO_METHOD *biom) +{ + git__free(biom); +} + +int BIO_meth_set_write__legacy(BIO_METHOD *biom, int (*write) (BIO *, const char *, int)) +{ + biom->bwrite = write; + return 1; +} + +int BIO_meth_set_read__legacy(BIO_METHOD *biom, int (*read) (BIO *, char *, int)) +{ + biom->bread = read; + return 1; +} + +int BIO_meth_set_puts__legacy(BIO_METHOD *biom, int (*puts) (BIO *, const char *)) +{ + biom->bputs = puts; + return 1; +} + +int BIO_meth_set_gets__legacy(BIO_METHOD *biom, int (*gets) (BIO *, char *, int)) + +{ + biom->bgets = gets; + return 1; +} + +int BIO_meth_set_ctrl__legacy(BIO_METHOD *biom, long (*ctrl) (BIO *, int, long, void *)) +{ + biom->ctrl = ctrl; + return 1; +} + +int BIO_meth_set_create__legacy(BIO_METHOD *biom, int (*create) (BIO *)) +{ + biom->create = create; + return 1; +} + +int BIO_meth_set_destroy__legacy(BIO_METHOD *biom, int (*destroy) (BIO *)) +{ + biom->destroy = destroy; + return 1; +} + +int BIO_get_new_index__legacy(void) +{ + /* This exists as of 1.1 so before we'd just have 0 */ + return 0; +} + +void BIO_set_init__legacy(BIO *b, int init) +{ + b->init = init; +} + +void BIO_set_data__legacy(BIO *a, void *ptr) +{ + a->ptr = ptr; +} + +void *BIO_get_data__legacy(BIO *a) +{ + return a->ptr; +} + +const unsigned char *ASN1_STRING_get0_data__legacy(const ASN1_STRING *x) +{ + return ASN1_STRING_data((ASN1_STRING *)x); +} + +long SSL_CTX_set_options__legacy(SSL_CTX *ctx, long op) +{ + return SSL_CTX_ctrl(ctx, SSL_CTRL_OPTIONS, op, NULL); +} + +# if defined(GIT_THREADS) +static git_mutex *openssl_locks; + +static void openssl_locking_function(int mode, int n, const char *file, int line) +{ + int lock; + + GIT_UNUSED(file); + GIT_UNUSED(line); + + lock = mode & CRYPTO_LOCK; + + if (lock) + (void)git_mutex_lock(&openssl_locks[n]); + else + git_mutex_unlock(&openssl_locks[n]); +} + +static void shutdown_ssl_locking(void) +{ + int num_locks, i; + + num_locks = CRYPTO_num_locks(); + CRYPTO_set_locking_callback(NULL); + + for (i = 0; i < num_locks; ++i) + git_mutex_free(&openssl_locks[i]); + git__free(openssl_locks); +} + +static void threadid_cb(CRYPTO_THREADID *threadid) +{ + GIT_UNUSED(threadid); + CRYPTO_THREADID_set_numeric(threadid, git_thread_currentid()); +} + +int git_openssl_set_locking(void) +{ + int num_locks, i; + +#ifndef GIT_THREADS + git_error_set(GIT_ERROR_THREAD, "libgit2 was not built with threads"); + return -1; +#endif + +#ifdef GIT_OPENSSL_DYNAMIC + /* + * This function is required on legacy versions of OpenSSL; when building + * with dynamically-loaded OpenSSL, we detect whether we loaded it or not. + */ + if (!CRYPTO_set_locking_callback) + return 0; +#endif + + CRYPTO_THREADID_set_callback(threadid_cb); + + num_locks = CRYPTO_num_locks(); + openssl_locks = git__calloc(num_locks, sizeof(git_mutex)); + GIT_ERROR_CHECK_ALLOC(openssl_locks); + + for (i = 0; i < num_locks; i++) { + if (git_mutex_init(&openssl_locks[i]) != 0) { + git_error_set(GIT_ERROR_SSL, "failed to initialize openssl locks"); + return -1; + } + } + + CRYPTO_set_locking_callback(openssl_locking_function); + return git_runtime_shutdown_register(shutdown_ssl_locking); +} +#endif /* GIT_THREADS */ + +#endif /* GIT_OPENSSL_LEGACY || GIT_OPENSSL_DYNAMIC */ diff --git a/src/libgit2/streams/openssl_legacy.h b/src/libgit2/streams/openssl_legacy.h new file mode 100644 index 0000000..e6dae95 --- /dev/null +++ b/src/libgit2/streams/openssl_legacy.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_streams_openssl_legacy_h__ +#define INCLUDE_streams_openssl_legacy_h__ + +#include "streams/openssl_dynamic.h" + +#if defined(GIT_OPENSSL) && !defined(GIT_OPENSSL_DYNAMIC) +# include +# include +# include +# include + +# if (defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER < 0x10100000L) || \ + (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L) +# define GIT_OPENSSL_LEGACY +# endif +#endif + +#if defined(GIT_OPENSSL_LEGACY) && !defined(GIT_OPENSSL_DYNAMIC) +# define OPENSSL_init_ssl OPENSSL_init_ssl__legacy +# define BIO_meth_new BIO_meth_new__legacy +# define BIO_meth_free BIO_meth_free__legacy +# define BIO_meth_set_write BIO_meth_set_write__legacy +# define BIO_meth_set_read BIO_meth_set_read__legacy +# define BIO_meth_set_puts BIO_meth_set_puts__legacy +# define BIO_meth_set_gets BIO_meth_set_gets__legacy +# define BIO_meth_set_ctrl BIO_meth_set_ctrl__legacy +# define BIO_meth_set_create BIO_meth_set_create__legacy +# define BIO_meth_set_destroy BIO_meth_set_destroy__legacy +# define BIO_get_new_index BIO_get_new_index__legacy +# define BIO_set_data BIO_set_data__legacy +# define BIO_set_init BIO_set_init__legacy +# define BIO_get_data BIO_get_data__legacy +# define ASN1_STRING_get0_data ASN1_STRING_get0_data__legacy +#endif + +#if defined(GIT_OPENSSL_LEGACY) || defined(GIT_OPENSSL_DYNAMIC) + +extern int OPENSSL_init_ssl__legacy(uint64_t opts, const void *settings); +extern BIO_METHOD *BIO_meth_new__legacy(int type, const char *name); +extern void BIO_meth_free__legacy(BIO_METHOD *biom); +extern int BIO_meth_set_write__legacy(BIO_METHOD *biom, int (*write) (BIO *, const char *, int)); +extern int BIO_meth_set_read__legacy(BIO_METHOD *biom, int (*read) (BIO *, char *, int)); +extern int BIO_meth_set_puts__legacy(BIO_METHOD *biom, int (*puts) (BIO *, const char *)); +extern int BIO_meth_set_gets__legacy(BIO_METHOD *biom, int (*gets) (BIO *, char *, int)); +extern int BIO_meth_set_ctrl__legacy(BIO_METHOD *biom, long (*ctrl) (BIO *, int, long, void *)); +extern int BIO_meth_set_create__legacy(BIO_METHOD *biom, int (*create) (BIO *)); +extern int BIO_meth_set_destroy__legacy(BIO_METHOD *biom, int (*destroy) (BIO *)); +extern int BIO_get_new_index__legacy(void); +extern void BIO_set_data__legacy(BIO *a, void *ptr); +extern void BIO_set_init__legacy(BIO *b, int init); +extern void *BIO_get_data__legacy(BIO *a); +extern const unsigned char *ASN1_STRING_get0_data__legacy(const ASN1_STRING *x); +extern long SSL_CTX_set_options__legacy(SSL_CTX *ctx, long op); + +#endif + +#endif diff --git a/src/libgit2/streams/registry.c b/src/libgit2/streams/registry.c new file mode 100644 index 0000000..e60e1cd --- /dev/null +++ b/src/libgit2/streams/registry.c @@ -0,0 +1,119 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "streams/registry.h" + +#include "runtime.h" +#include "streams/tls.h" +#include "streams/mbedtls.h" +#include "streams/openssl.h" +#include "streams/stransport.h" + +struct stream_registry { + git_rwlock lock; + git_stream_registration callbacks; + git_stream_registration tls_callbacks; +}; + +static struct stream_registry stream_registry; + +static void shutdown_stream_registry(void) +{ + git_rwlock_free(&stream_registry.lock); +} + +int git_stream_registry_global_init(void) +{ + if (git_rwlock_init(&stream_registry.lock) < 0) + return -1; + + return git_runtime_shutdown_register(shutdown_stream_registry); +} + +GIT_INLINE(void) stream_registration_cpy( + git_stream_registration *target, + git_stream_registration *src) +{ + if (src) + memcpy(target, src, sizeof(git_stream_registration)); + else + memset(target, 0, sizeof(git_stream_registration)); +} + +int git_stream_registry_lookup(git_stream_registration *out, git_stream_t type) +{ + git_stream_registration *target; + int error = GIT_ENOTFOUND; + + GIT_ASSERT_ARG(out); + + switch(type) { + case GIT_STREAM_STANDARD: + target = &stream_registry.callbacks; + break; + case GIT_STREAM_TLS: + target = &stream_registry.tls_callbacks; + break; + default: + git_error_set(GIT_ERROR_INVALID, "invalid stream type"); + return -1; + } + + if (git_rwlock_rdlock(&stream_registry.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock stream registry"); + return -1; + } + + if (target->init) { + stream_registration_cpy(out, target); + error = 0; + } + + git_rwlock_rdunlock(&stream_registry.lock); + return error; +} + +int git_stream_register(git_stream_t type, git_stream_registration *registration) +{ + GIT_ASSERT(!registration || registration->init); + + GIT_ERROR_CHECK_VERSION(registration, GIT_STREAM_VERSION, "stream_registration"); + + if (git_rwlock_wrlock(&stream_registry.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock stream registry"); + return -1; + } + + if ((type & GIT_STREAM_STANDARD) == GIT_STREAM_STANDARD) + stream_registration_cpy(&stream_registry.callbacks, registration); + + if ((type & GIT_STREAM_TLS) == GIT_STREAM_TLS) + stream_registration_cpy(&stream_registry.tls_callbacks, registration); + + git_rwlock_wrunlock(&stream_registry.lock); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_stream_register_tls( + int GIT_CALLBACK(ctor)(git_stream **out, const char *host, const char *port)) +{ + git_stream_registration registration = {0}; + + if (ctor) { + registration.version = GIT_STREAM_VERSION; + registration.init = ctor; + registration.wrap = NULL; + + return git_stream_register(GIT_STREAM_TLS, ®istration); + } else { + return git_stream_register(GIT_STREAM_TLS, NULL); + } +} +#endif diff --git a/src/libgit2/streams/registry.h b/src/libgit2/streams/registry.h new file mode 100644 index 0000000..adc2b8b --- /dev/null +++ b/src/libgit2/streams/registry.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_streams_registry_h__ +#define INCLUDE_streams_registry_h__ + +#include "common.h" +#include "git2/sys/stream.h" + +/** Configure stream registry. */ +int git_stream_registry_global_init(void); + +/** Lookup a stream registration. */ +extern int git_stream_registry_lookup(git_stream_registration *out, git_stream_t type); + +#endif diff --git a/src/libgit2/streams/schannel.c b/src/libgit2/streams/schannel.c new file mode 100644 index 0000000..f096158 --- /dev/null +++ b/src/libgit2/streams/schannel.c @@ -0,0 +1,715 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "streams/schannel.h" + +#ifdef GIT_SCHANNEL + +#define SECURITY_WIN32 + +#include +#include +#include + +#include "stream.h" +#include "streams/socket.h" + +#ifndef SP_PROT_TLS1_2_CLIENT +# define SP_PROT_TLS1_2_CLIENT 2048 +#endif + +#ifndef SP_PROT_TLS1_3_CLIENT +# define SP_PROT_TLS1_3_CLIENT 8192 +#endif + +#ifndef SECBUFFER_ALERT +# define SECBUFFER_ALERT 17 +#endif + +#define READ_BLOCKSIZE (16 * 1024) + +typedef enum { + STATE_NONE = 0, + STATE_CRED = 1, + STATE_CONTEXT = 2, + STATE_CERTIFICATE = 3 +} schannel_state; + +typedef struct { + git_stream parent; + git_stream *io; + int owned; + bool connected; + wchar_t *host_w; + + schannel_state state; + + CredHandle cred; + CtxtHandle context; + SecPkgContext_StreamSizes stream_sizes; + + CERT_CONTEXT *certificate; + const CERT_CHAIN_CONTEXT *cert_chain; + git_cert_x509 x509; + + git_str plaintext_in; + git_str ciphertext_in; +} schannel_stream; + +static int connect_context(schannel_stream *st) +{ + SCHANNEL_CRED cred = { 0 }; + SECURITY_STATUS status = SEC_E_INTERNAL_ERROR; + DWORD context_flags; + static size_t MAX_RETRIES = 1024; + size_t retries; + ssize_t read_len; + int error = 0; + + if (st->owned && (error = git_stream_connect(st->io)) < 0) + return error; + + cred.dwVersion = SCHANNEL_CRED_VERSION; + cred.dwFlags = SCH_CRED_IGNORE_NO_REVOCATION_CHECK | + SCH_CRED_IGNORE_REVOCATION_OFFLINE | + SCH_CRED_MANUAL_CRED_VALIDATION | + SCH_CRED_NO_DEFAULT_CREDS | + SCH_CRED_NO_SERVERNAME_CHECK; + cred.grbitEnabledProtocols = SP_PROT_TLS1_2_CLIENT | + SP_PROT_TLS1_3_CLIENT; + + if (AcquireCredentialsHandleW(NULL, SCHANNEL_NAME_W, + SECPKG_CRED_OUTBOUND, NULL, &cred, NULL, + NULL, &st->cred, NULL) != SEC_E_OK) { + git_error_set(GIT_ERROR_OS, "could not acquire credentials handle"); + return -1; + } + + st->state = STATE_CRED; + + context_flags = ISC_REQ_ALLOCATE_MEMORY | + ISC_REQ_CONFIDENTIALITY | + ISC_REQ_REPLAY_DETECT | + ISC_REQ_SEQUENCE_DETECT | + ISC_REQ_STREAM; + + for (retries = 0; retries < MAX_RETRIES; retries++) { + SecBuffer input_buf[] = { + { (unsigned long)st->ciphertext_in.size, + SECBUFFER_TOKEN, + st->ciphertext_in.size ? st->ciphertext_in.ptr : NULL }, + { 0, SECBUFFER_EMPTY, NULL } + }; + SecBuffer output_buf[] = { { 0, SECBUFFER_TOKEN, NULL }, + { 0, SECBUFFER_ALERT, NULL } }; + + SecBufferDesc input_buf_desc = { SECBUFFER_VERSION, 2, input_buf }; + SecBufferDesc output_buf_desc = { SECBUFFER_VERSION, 2, output_buf }; + + status = InitializeSecurityContextW(&st->cred, + retries ? &st->context : NULL, st->host_w, + context_flags, 0, 0, retries ? &input_buf_desc : NULL, 0, + retries ? NULL : &st->context, &output_buf_desc, + &context_flags, NULL); + + if (status == SEC_E_OK || status == SEC_I_CONTINUE_NEEDED) { + st->state = STATE_CONTEXT; + + if (output_buf[0].cbBuffer > 0) { + error = git_stream__write_full(st->io, + output_buf[0].pvBuffer, + output_buf[0].cbBuffer, 0); + + FreeContextBuffer(output_buf[0].pvBuffer); + } + + /* handle any leftover, unprocessed data */ + if (input_buf[1].BufferType == SECBUFFER_EXTRA) { + GIT_ASSERT(st->ciphertext_in.size > input_buf[1].cbBuffer); + + git_str_consume_bytes(&st->ciphertext_in, + st->ciphertext_in.size - input_buf[1].cbBuffer); + } else { + git_str_clear(&st->ciphertext_in); + } + + if (error < 0 || status == SEC_E_OK) + break; + } else if (status == SEC_E_INCOMPLETE_MESSAGE) { + /* we need additional data from the client; */ + if (git_str_grow_by(&st->ciphertext_in, READ_BLOCKSIZE) < 0) { + error = -1; + break; + } + + if ((read_len = git_stream_read(st->io, + st->ciphertext_in.ptr + st->ciphertext_in.size, + (st->ciphertext_in.asize - st->ciphertext_in.size))) < 0) { + error = -1; + break; + } + + GIT_ASSERT((size_t)read_len <= + st->ciphertext_in.asize - st->ciphertext_in.size); + st->ciphertext_in.size += read_len; + } else { + git_error_set(GIT_ERROR_OS, + "could not initialize security context"); + error = -1; + break; + } + + GIT_ASSERT(st->ciphertext_in.size < ULONG_MAX); + } + + if (retries == MAX_RETRIES) { + git_error_set(GIT_ERROR_SSL, + "could not initialize security context: too many retries"); + error = -1; + } + + if (!error) { + if (QueryContextAttributesW(&st->context, + SECPKG_ATTR_STREAM_SIZES, + &st->stream_sizes) != SEC_E_OK) { + git_error_set(GIT_ERROR_SSL, + "could not query stream sizes"); + error = -1; + } + } + + return error; +} + +static int set_certificate_lookup_error(DWORD status) +{ + switch (status) { + case CERT_TRUST_IS_NOT_TIME_VALID: + git_error_set(GIT_ERROR_SSL, + "certificate is expired or not yet valid"); + break; + case CERT_TRUST_IS_REVOKED: + git_error_set(GIT_ERROR_SSL, "certificate is revoked"); + break; + case CERT_TRUST_IS_NOT_SIGNATURE_VALID: + case CERT_TRUST_IS_NOT_VALID_FOR_USAGE: + case CERT_TRUST_INVALID_EXTENSION: + case CERT_TRUST_INVALID_POLICY_CONSTRAINTS: + case CERT_TRUST_INVALID_BASIC_CONSTRAINTS: + case CERT_TRUST_INVALID_NAME_CONSTRAINTS: + case CERT_TRUST_HAS_NOT_SUPPORTED_NAME_CONSTRAINT: + case CERT_TRUST_HAS_NOT_DEFINED_NAME_CONSTRAINT: + case CERT_TRUST_HAS_NOT_PERMITTED_NAME_CONSTRAINT: + case CERT_TRUST_HAS_EXCLUDED_NAME_CONSTRAINT: + case CERT_TRUST_NO_ISSUANCE_CHAIN_POLICY: + case CERT_TRUST_HAS_NOT_SUPPORTED_CRITICAL_EXT: + git_error_set(GIT_ERROR_SSL, "certificate is not valid"); + break; + case CERT_TRUST_IS_UNTRUSTED_ROOT: + case CERT_TRUST_IS_CYCLIC: + case CERT_TRUST_IS_EXPLICIT_DISTRUST: + git_error_set(GIT_ERROR_SSL, "certificate is not trusted"); + break; + case CERT_TRUST_REVOCATION_STATUS_UNKNOWN: + git_error_set(GIT_ERROR_SSL, + "certificate revocation status could not be verified"); + break; + case CERT_TRUST_IS_OFFLINE_REVOCATION: + git_error_set(GIT_ERROR_SSL, + "certificate revocation is offline or stale"); + break; + case CERT_TRUST_HAS_WEAK_SIGNATURE: + git_error_set(GIT_ERROR_SSL, "certificate has a weak signature"); + break; + default: + git_error_set(GIT_ERROR_SSL, + "unknown certificate lookup failure: %d", status); + return -1; + } + + return GIT_ECERTIFICATE; +} + +static int set_certificate_validation_error(DWORD status) +{ + switch (status) { + case TRUST_E_CERT_SIGNATURE: + git_error_set(GIT_ERROR_SSL, + "the certificate cannot be verified"); + break; + case CRYPT_E_REVOKED: + git_error_set(GIT_ERROR_SSL, + "the certificate or signature has been revoked"); + break; + case CERT_E_UNTRUSTEDROOT: + git_error_set(GIT_ERROR_SSL, + "the certificate root is not trusted"); + break; + case CERT_E_UNTRUSTEDTESTROOT: + git_error_set(GIT_ERROR_SSL, + "the certificate root is a test certificate"); + break; + case CERT_E_CHAINING: + git_error_set(GIT_ERROR_SSL, + "the certificate chain is invalid"); + break; + case CERT_E_WRONG_USAGE: + case CERT_E_PURPOSE: + git_error_set(GIT_ERROR_SSL, + "the certificate is not valid for this usage"); + break; + case CERT_E_EXPIRED: + git_error_set(GIT_ERROR_SSL, + "certificate is expired or not yet valid"); + break; + case CERT_E_INVALID_NAME: + case CERT_E_CN_NO_MATCH: + git_error_set(GIT_ERROR_SSL, + "certificate is not valid for this hostname"); + break; + case CERT_E_INVALID_POLICY: + case TRUST_E_BASIC_CONSTRAINTS: + case CERT_E_CRITICAL: + case CERT_E_VALIDITYPERIODNESTING: + git_error_set(GIT_ERROR_SSL, "certificate is not valid"); + break; + case CRYPT_E_NO_REVOCATION_CHECK: + git_error_set(GIT_ERROR_SSL, + "certificate revocation status could not be verified"); + break; + case CRYPT_E_REVOCATION_OFFLINE: + git_error_set(GIT_ERROR_SSL, + "certificate revocation is offline or stale"); + break; + case CERT_E_ROLE: + git_error_set(GIT_ERROR_SSL, "certificate authority is not valid"); + break; + default: + git_error_set(GIT_ERROR_SSL, + "unknown certificate policy checking failure: %d", + status); + return -1; + } + + return GIT_ECERTIFICATE; +} + +static int check_certificate(schannel_stream* st) +{ + CERT_CHAIN_PARA cert_chain_parameters; + SSL_EXTRA_CERT_CHAIN_POLICY_PARA ssl_policy_parameters; + CERT_CHAIN_POLICY_PARA cert_policy_parameters = + { sizeof(CERT_CHAIN_POLICY_PARA), 0, &ssl_policy_parameters }; + CERT_CHAIN_POLICY_STATUS cert_policy_status; + + memset(&cert_chain_parameters, 0, sizeof(CERT_CHAIN_PARA)); + cert_chain_parameters.cbSize = sizeof(CERT_CHAIN_PARA); + + if (QueryContextAttributesW(&st->context, + SECPKG_ATTR_REMOTE_CERT_CONTEXT, + &st->certificate) != SEC_E_OK) { + git_error_set(GIT_ERROR_OS, + "could not query remote certificate context"); + return -1; + } + + /* TODO: do we really want to do revokcation checking ? */ + if (!CertGetCertificateChain(NULL, st->certificate, NULL, + st->certificate->hCertStore, &cert_chain_parameters, + CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT, + NULL, &st->cert_chain)) { + git_error_set(GIT_ERROR_OS, "could not query remote certificate chain"); + CertFreeCertificateContext(st->certificate); + return -1; + } + + st->state = STATE_CERTIFICATE; + + /* Set up the x509 certificate data for future callbacks */ + + st->x509.parent.cert_type = GIT_CERT_X509; + st->x509.data = st->certificate->pbCertEncoded; + st->x509.len = st->certificate->cbCertEncoded; + + /* Handle initial certificate validation */ + + if (st->cert_chain->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) + return set_certificate_lookup_error(st->cert_chain->TrustStatus.dwErrorStatus); + + ssl_policy_parameters.cbSize = sizeof(SSL_EXTRA_CERT_CHAIN_POLICY_PARA); + ssl_policy_parameters.dwAuthType = AUTHTYPE_SERVER; + ssl_policy_parameters.pwszServerName = st->host_w; + + if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, + st->cert_chain, &cert_policy_parameters, + &cert_policy_status)) { + git_error_set(GIT_ERROR_OS, "could not verify certificate chain policy"); + return -1; + } + + if (cert_policy_status.dwError != SEC_E_OK) + return set_certificate_validation_error(cert_policy_status.dwError); + + return 0; +} + +static int schannel_connect(git_stream *stream) +{ + schannel_stream *st = (schannel_stream *)stream; + int error; + + GIT_ASSERT(st->state == STATE_NONE); + + if ((error = connect_context(st)) < 0 || + (error = check_certificate(st)) < 0) + return error; + + st->connected = 1; + return 0; +} + +static int schannel_certificate(git_cert **out, git_stream *stream) +{ + schannel_stream *st = (schannel_stream *)stream; + + *out = &st->x509.parent; + return 0; +} + +static int schannel_set_proxy( + git_stream *stream, + const git_proxy_options *proxy_options) +{ + schannel_stream *st = (schannel_stream *)stream; + return git_stream_set_proxy(st->io, proxy_options); +} + +static ssize_t schannel_write( + git_stream *stream, + const char *data, + size_t data_len, + int flags) +{ + schannel_stream *st = (schannel_stream *)stream; + SecBuffer encrypt_buf[3]; + SecBufferDesc encrypt_buf_desc = { SECBUFFER_VERSION, 3, encrypt_buf }; + git_str ciphertext_out = GIT_STR_INIT; + ssize_t total_len = 0; + + GIT_UNUSED(flags); + + if (data_len > SSIZE_MAX) + data_len = SSIZE_MAX; + + git_str_init(&ciphertext_out, + st->stream_sizes.cbHeader + + st->stream_sizes.cbMaximumMessage + + st->stream_sizes.cbTrailer); + + while (data_len > 0) { + size_t message_len = min(data_len, st->stream_sizes.cbMaximumMessage); + size_t ciphertext_len, ciphertext_written = 0; + + encrypt_buf[0].BufferType = SECBUFFER_STREAM_HEADER; + encrypt_buf[0].cbBuffer = st->stream_sizes.cbHeader; + encrypt_buf[0].pvBuffer = ciphertext_out.ptr; + + encrypt_buf[1].BufferType = SECBUFFER_DATA; + encrypt_buf[1].cbBuffer = (unsigned long)message_len; + encrypt_buf[1].pvBuffer = + ciphertext_out.ptr + st->stream_sizes.cbHeader; + + encrypt_buf[2].BufferType = SECBUFFER_STREAM_TRAILER; + encrypt_buf[2].cbBuffer = st->stream_sizes.cbTrailer; + encrypt_buf[2].pvBuffer = + ciphertext_out.ptr + st->stream_sizes.cbHeader + + message_len; + + memcpy(ciphertext_out.ptr + st->stream_sizes.cbHeader, data, message_len); + + if (EncryptMessage(&st->context, 0, &encrypt_buf_desc, 0) != SEC_E_OK) { + git_error_set(GIT_ERROR_OS, "could not encrypt tls message"); + total_len = -1; + goto done; + } + + ciphertext_len = encrypt_buf[0].cbBuffer + + encrypt_buf[1].cbBuffer + + encrypt_buf[2].cbBuffer; + + while (ciphertext_written < ciphertext_len) { + ssize_t chunk_len = git_stream_write(st->io, + ciphertext_out.ptr + ciphertext_written, + ciphertext_len - ciphertext_written, 0); + + if (chunk_len < 0) { + total_len = -1; + goto done; + } + + ciphertext_len -= chunk_len; + ciphertext_written += chunk_len; + } + + total_len += message_len; + + data += message_len; + data_len -= message_len; + } + +done: + git_str_dispose(&ciphertext_out); + return total_len; +} + +static ssize_t schannel_read(git_stream *stream, void *_data, size_t data_len) +{ + schannel_stream *st = (schannel_stream *)stream; + char *data = (char *)_data; + SecBuffer decrypt_buf[4]; + SecBufferDesc decrypt_buf_desc = { SECBUFFER_VERSION, 4, decrypt_buf }; + SECURITY_STATUS status; + ssize_t chunk_len, total_len = 0; + + if (data_len > SSIZE_MAX) + data_len = SSIZE_MAX; + + /* + * Loop until we have some bytes to return - we may have decrypted + * bytes queued or ciphertext from the wire that we can decrypt and + * return. Return any queued bytes if they're available to avoid a + * network read, which may block. We may return less than the + * caller requested, and they can retry for an actual network + */ + while ((size_t)total_len < data_len) { + if (st->plaintext_in.size > 0) { + size_t copy_len = min(st->plaintext_in.size, data_len); + + memcpy(data, st->plaintext_in.ptr, copy_len); + git_str_consume_bytes(&st->plaintext_in, copy_len); + + data += copy_len; + data_len -= copy_len; + + total_len += copy_len; + + continue; + } + + if (st->ciphertext_in.size > 0) { + decrypt_buf[0].BufferType = SECBUFFER_DATA; + decrypt_buf[0].cbBuffer = (unsigned long)min(st->ciphertext_in.size, ULONG_MAX); + decrypt_buf[0].pvBuffer = st->ciphertext_in.ptr; + + decrypt_buf[1].BufferType = SECBUFFER_EMPTY; + decrypt_buf[1].cbBuffer = 0; + decrypt_buf[1].pvBuffer = NULL; + + decrypt_buf[2].BufferType = SECBUFFER_EMPTY; + decrypt_buf[2].cbBuffer = 0; + decrypt_buf[2].pvBuffer = NULL; + + decrypt_buf[3].BufferType = SECBUFFER_EMPTY; + decrypt_buf[3].cbBuffer = 0; + decrypt_buf[3].pvBuffer = NULL; + + status = DecryptMessage(&st->context, &decrypt_buf_desc, 0, NULL); + + if (status == SEC_E_OK) { + GIT_ASSERT(decrypt_buf[0].BufferType == SECBUFFER_STREAM_HEADER); + GIT_ASSERT(decrypt_buf[1].BufferType == SECBUFFER_DATA); + GIT_ASSERT(decrypt_buf[2].BufferType == SECBUFFER_STREAM_TRAILER); + + if (git_str_put(&st->plaintext_in, decrypt_buf[1].pvBuffer, decrypt_buf[1].cbBuffer) < 0) { + total_len = -1; + goto done; + } + + if (decrypt_buf[3].BufferType == SECBUFFER_EXTRA) { + git_str_consume_bytes(&st->ciphertext_in, (st->ciphertext_in.size - decrypt_buf[3].cbBuffer)); + } else { + git_str_clear(&st->ciphertext_in); + } + + continue; + } else if (status == SEC_E_CONTEXT_EXPIRED) { + break; + } else if (status != SEC_E_INCOMPLETE_MESSAGE) { + git_error_set(GIT_ERROR_SSL, "could not decrypt tls message"); + total_len = -1; + goto done; + } + } + + if (total_len != 0) + break; + + if (git_str_grow_by(&st->ciphertext_in, READ_BLOCKSIZE) < 0) { + total_len = -1; + goto done; + } + + if ((chunk_len = git_stream_read(st->io, st->ciphertext_in.ptr + st->ciphertext_in.size, st->ciphertext_in.asize - st->ciphertext_in.size)) < 0) { + total_len = -1; + goto done; + } + + st->ciphertext_in.size += chunk_len; + } + +done: + return total_len; +} + +static int schannel_close(git_stream *stream) +{ + schannel_stream *st = (schannel_stream *)stream; + int error = 0; + + if (st->connected) { + SecBuffer shutdown_buf; + SecBufferDesc shutdown_buf_desc = + { SECBUFFER_VERSION, 1, &shutdown_buf }; + DWORD shutdown_message = SCHANNEL_SHUTDOWN, shutdown_flags; + + shutdown_buf.BufferType = SECBUFFER_TOKEN; + shutdown_buf.cbBuffer = sizeof(DWORD); + shutdown_buf.pvBuffer = &shutdown_message; + + if (ApplyControlToken(&st->context, &shutdown_buf_desc) != SEC_E_OK) { + git_error_set(GIT_ERROR_SSL, "could not shutdown stream"); + error = -1; + } + + shutdown_buf.BufferType = SECBUFFER_TOKEN; + shutdown_buf.cbBuffer = 0; + shutdown_buf.pvBuffer = NULL; + + shutdown_flags = ISC_REQ_ALLOCATE_MEMORY | + ISC_REQ_CONFIDENTIALITY | + ISC_REQ_REPLAY_DETECT | + ISC_REQ_SEQUENCE_DETECT | + ISC_REQ_STREAM; + + if (InitializeSecurityContext(&st->cred, &st->context, + NULL, shutdown_flags, 0, 0, + &shutdown_buf_desc, 0, NULL, + &shutdown_buf_desc, &shutdown_flags, + NULL) == SEC_E_OK) { + if (shutdown_buf.cbBuffer > 0) { + if (git_stream__write_full(st->io, + shutdown_buf.pvBuffer, + shutdown_buf.cbBuffer, 0) < 0) + error = -1; + + FreeContextBuffer(shutdown_buf.pvBuffer); + } + } + } + + st->connected = false; + + if (st->owned && git_stream_close(st->io) < 0) + error = -1; + + return error; +} + +static void schannel_free(git_stream *stream) +{ + schannel_stream *st = (schannel_stream *)stream; + + if (st->state >= STATE_CERTIFICATE) { + CertFreeCertificateContext(st->certificate); + CertFreeCertificateChain(st->cert_chain); + } + + if (st->state >= STATE_CONTEXT) + DeleteSecurityContext(&st->context); + + if (st->state >= STATE_CRED) + FreeCredentialsHandle(&st->cred); + + st->state = STATE_NONE; + + git_str_dispose(&st->ciphertext_in); + git_str_dispose(&st->plaintext_in); + + git__free(st->host_w); + + if (st->owned) + git_stream_free(st->io); + + git__free(st); +} + +static int schannel_stream_wrap( + git_stream **out, + git_stream *in, + const char *host, + int owned) +{ + schannel_stream *st; + + st = git__calloc(1, sizeof(schannel_stream)); + GIT_ERROR_CHECK_ALLOC(st); + + st->io = in; + st->owned = owned; + + if (git_utf8_to_16_alloc(&st->host_w, host) < 0) { + git__free(st); + return -1; + } + + st->parent.version = GIT_STREAM_VERSION; + st->parent.encrypted = 1; + st->parent.proxy_support = git_stream_supports_proxy(st->io); + st->parent.connect = schannel_connect; + st->parent.certificate = schannel_certificate; + st->parent.set_proxy = schannel_set_proxy; + st->parent.read = schannel_read; + st->parent.write = schannel_write; + st->parent.close = schannel_close; + st->parent.free = schannel_free; + + *out = (git_stream *)st; + return 0; +} + +extern int git_schannel_stream_new( + git_stream **out, + const char *host, + const char *port) +{ + git_stream *stream; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(host); + GIT_ASSERT_ARG(port); + + if ((error = git_socket_stream_new(&stream, host, port)) < 0) + return error; + + if ((error = schannel_stream_wrap(out, stream, host, 1)) < 0) { + git_stream_close(stream); + git_stream_free(stream); + } + + return error; +} + +extern int git_schannel_stream_wrap( + git_stream **out, + git_stream *in, + const char *host) +{ + return schannel_stream_wrap(out, in, host, 0); +} + +#endif diff --git a/src/libgit2/streams/schannel.h b/src/libgit2/streams/schannel.h new file mode 100644 index 0000000..3584970 --- /dev/null +++ b/src/libgit2/streams/schannel.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_steams_schannel_h__ +#define INCLUDE_steams_schannel_h__ + +#include "common.h" + +#include "git2/sys/stream.h" + +#ifdef GIT_SCHANNEL + +extern int git_schannel_stream_new( + git_stream **out, + const char *host, + const char *port); + +extern int git_schannel_stream_wrap( + git_stream **out, + git_stream *in, + const char *host); + +#endif + +#endif diff --git a/src/libgit2/streams/socket.c b/src/libgit2/streams/socket.c new file mode 100644 index 0000000..a463312 --- /dev/null +++ b/src/libgit2/streams/socket.c @@ -0,0 +1,428 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "streams/socket.h" + +#include "posix.h" +#include "registry.h" +#include "runtime.h" +#include "stream.h" + +#ifndef _WIN32 +# include +# include +# include +# include +# include +# include +# include +#else +# include +# include +# ifdef _MSC_VER +# pragma comment(lib, "ws2_32") +# endif +#endif + +int git_socket_stream__connect_timeout = 0; +int git_socket_stream__timeout = 0; + +#ifdef GIT_WIN32 +static void net_set_error(const char *str) +{ + int error = WSAGetLastError(); + char * win32_error = git_win32_get_error_message(error); + + if (win32_error) { + git_error_set(GIT_ERROR_NET, "%s: %s", str, win32_error); + git__free(win32_error); + } else { + git_error_set(GIT_ERROR_NET, "%s", str); + } +} +#else +static void net_set_error(const char *str) +{ + git_error_set(GIT_ERROR_NET, "%s: %s", str, strerror(errno)); +} +#endif + +static int close_socket(GIT_SOCKET s) +{ + if (s == INVALID_SOCKET) + return 0; + +#ifdef GIT_WIN32 + if (closesocket(s) != 0) { + net_set_error("could not close socket"); + return -1; + } + + return 0; +#else + return close(s); +#endif + +} + +static int set_nonblocking(GIT_SOCKET s) +{ +#ifdef GIT_WIN32 + unsigned long nonblocking = 1; + + if (ioctlsocket(s, FIONBIO, &nonblocking) != 0) { + net_set_error("could not set socket non-blocking"); + return -1; + } +#else + int flags; + + if ((flags = fcntl(s, F_GETFL, 0)) == -1) { + net_set_error("could not query socket flags"); + return -1; + } + + flags |= O_NONBLOCK; + + if (fcntl(s, F_SETFL, flags) != 0) { + net_set_error("could not set socket non-blocking"); + return -1; + } +#endif + + return 0; +} + +/* Promote a sockerr to an errno for our error handling routines */ +static int handle_sockerr(GIT_SOCKET socket) +{ + int sockerr; + socklen_t errlen = sizeof(sockerr); + + if (getsockopt(socket, SOL_SOCKET, SO_ERROR, + (void *)&sockerr, &errlen) < 0) + return -1; + + if (sockerr == ETIMEDOUT) + return GIT_TIMEOUT; + + errno = sockerr; + return -1; +} + +GIT_INLINE(bool) connect_would_block(int error) +{ +#ifdef GIT_WIN32 + if (error == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK) + return true; +#endif + + if (error == -1 && errno == EINPROGRESS) + return true; + + return false; +} + +static int connect_with_timeout( + GIT_SOCKET socket, + const struct sockaddr *address, + socklen_t address_len, + int timeout) +{ + struct pollfd fd; + int error; + + if (timeout && (error = set_nonblocking(socket)) < 0) + return error; + + error = connect(socket, address, address_len); + + if (error == 0 || !connect_would_block(error)) + return error; + + fd.fd = socket; + fd.events = POLLOUT; + fd.revents = 0; + + error = p_poll(&fd, 1, timeout); + + if (error == 0) { + return GIT_TIMEOUT; + } else if (error != 1) { + return -1; + } else if ((fd.revents & (POLLPRI | POLLHUP | POLLERR))) { + return handle_sockerr(socket); + } else if ((fd.revents & POLLOUT) != POLLOUT) { + git_error_set(GIT_ERROR_NET, + "unknown error while polling for connect: %d", + fd.revents); + return -1; + } + + return 0; +} + +static int socket_connect(git_stream *stream) +{ + git_socket_stream *st = (git_socket_stream *) stream; + GIT_SOCKET s = INVALID_SOCKET; + struct addrinfo *info = NULL, *p; + struct addrinfo hints; + int error; + + memset(&hints, 0x0, sizeof(struct addrinfo)); + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_UNSPEC; + + if ((error = p_getaddrinfo(st->host, st->port, &hints, &info)) != 0) { + git_error_set(GIT_ERROR_NET, + "failed to resolve address for %s: %s", + st->host, p_gai_strerror(error)); + return -1; + } + + for (p = info; p != NULL; p = p->ai_next) { + s = socket(p->ai_family, p->ai_socktype | SOCK_CLOEXEC, p->ai_protocol); + + if (s == INVALID_SOCKET) + continue; + + error = connect_with_timeout(s, p->ai_addr, + (socklen_t)p->ai_addrlen, + st->parent.connect_timeout); + + if (error == 0) + break; + + /* If we can't connect, try the next one */ + close_socket(s); + s = INVALID_SOCKET; + + if (error == GIT_TIMEOUT) + break; + } + + /* Oops, we couldn't connect to any address */ + if (s == INVALID_SOCKET) { + if (error == GIT_TIMEOUT) + git_error_set(GIT_ERROR_NET, "failed to connect to %s: Operation timed out", st->host); + else + git_error_set(GIT_ERROR_OS, "failed to connect to %s", st->host); + error = -1; + goto done; + } + + if (st->parent.timeout && !st->parent.connect_timeout && + (error = set_nonblocking(s)) < 0) + return error; + + st->s = s; + error = 0; + +done: + p_freeaddrinfo(info); + return error; +} + +static ssize_t socket_write( + git_stream *stream, + const char *data, + size_t len, + int flags) +{ + git_socket_stream *st = (git_socket_stream *) stream; + struct pollfd fd; + ssize_t ret; + + GIT_ASSERT(flags == 0); + GIT_UNUSED(flags); + + ret = p_send(st->s, data, len, 0); + + if (st->parent.timeout && ret < 0 && + (errno == EAGAIN || errno != EWOULDBLOCK)) { + fd.fd = st->s; + fd.events = POLLOUT; + fd.revents = 0; + + ret = p_poll(&fd, 1, st->parent.timeout); + + if (ret == 1) { + ret = p_send(st->s, data, len, 0); + } else if (ret == 0) { + git_error_set(GIT_ERROR_NET, + "could not write to socket: timed out"); + return GIT_TIMEOUT; + } + } + + if (ret < 0) { + net_set_error("error receiving data from socket"); + return -1; + } + + return ret; +} + +static ssize_t socket_read( + git_stream *stream, + void *data, + size_t len) +{ + git_socket_stream *st = (git_socket_stream *) stream; + struct pollfd fd; + ssize_t ret; + + ret = p_recv(st->s, data, len, 0); + + if (st->parent.timeout && ret < 0 && + (errno == EAGAIN || errno != EWOULDBLOCK)) { + fd.fd = st->s; + fd.events = POLLIN; + fd.revents = 0; + + ret = p_poll(&fd, 1, st->parent.timeout); + + if (ret == 1) { + ret = p_recv(st->s, data, len, 0); + } else if (ret == 0) { + git_error_set(GIT_ERROR_NET, + "could not read from socket: timed out"); + return GIT_TIMEOUT; + } + } + + if (ret < 0) { + net_set_error("error receiving data from socket"); + return -1; + } + + return ret; +} + +static int socket_close(git_stream *stream) +{ + git_socket_stream *st = (git_socket_stream *) stream; + int error; + + error = close_socket(st->s); + st->s = INVALID_SOCKET; + + return error; +} + +static void socket_free(git_stream *stream) +{ + git_socket_stream *st = (git_socket_stream *) stream; + + git__free(st->host); + git__free(st->port); + git__free(st); +} + +static int default_socket_stream_new( + git_stream **out, + const char *host, + const char *port) +{ + git_socket_stream *st; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(host); + GIT_ASSERT_ARG(port); + + st = git__calloc(1, sizeof(git_socket_stream)); + GIT_ERROR_CHECK_ALLOC(st); + + st->host = git__strdup(host); + GIT_ERROR_CHECK_ALLOC(st->host); + + if (port) { + st->port = git__strdup(port); + GIT_ERROR_CHECK_ALLOC(st->port); + } + + st->parent.version = GIT_STREAM_VERSION; + st->parent.timeout = git_socket_stream__timeout; + st->parent.connect_timeout = git_socket_stream__connect_timeout; + st->parent.connect = socket_connect; + st->parent.write = socket_write; + st->parent.read = socket_read; + st->parent.close = socket_close; + st->parent.free = socket_free; + st->s = INVALID_SOCKET; + + *out = (git_stream *) st; + return 0; +} + +int git_socket_stream_new( + git_stream **out, + const char *host, + const char *port) +{ + int (*init)(git_stream **, const char *, const char *) = NULL; + git_stream_registration custom = {0}; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(host); + GIT_ASSERT_ARG(port); + + if ((error = git_stream_registry_lookup(&custom, GIT_STREAM_STANDARD)) == 0) + init = custom.init; + else if (error == GIT_ENOTFOUND) + init = default_socket_stream_new; + else + return error; + + if (!init) { + git_error_set(GIT_ERROR_NET, "there is no socket stream available"); + return -1; + } + + return init(out, host, port); +} + +#ifdef GIT_WIN32 + +static void socket_stream_global_shutdown(void) +{ + WSACleanup(); +} + +int git_socket_stream_global_init(void) +{ + WORD winsock_version; + WSADATA wsa_data; + + winsock_version = MAKEWORD(2, 2); + + if (WSAStartup(winsock_version, &wsa_data) != 0) { + git_error_set(GIT_ERROR_OS, "could not initialize Windows Socket Library"); + return -1; + } + + if (LOBYTE(wsa_data.wVersion) != 2 || + HIBYTE(wsa_data.wVersion) != 2) { + git_error_set(GIT_ERROR_SSL, "Windows Socket Library does not support Winsock 2.2"); + return -1; + } + + return git_runtime_shutdown_register(socket_stream_global_shutdown); +} + +#else + +#include "stream.h" + +int git_socket_stream_global_init(void) +{ + return 0; +} + + #endif diff --git a/src/libgit2/streams/socket.h b/src/libgit2/streams/socket.h new file mode 100644 index 0000000..73e8de0 --- /dev/null +++ b/src/libgit2/streams/socket.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_streams_socket_h__ +#define INCLUDE_streams_socket_h__ + +#include "common.h" + +#include "stream.h" + +typedef struct { + git_stream parent; + char *host; + char *port; + GIT_SOCKET s; +} git_socket_stream; + +extern int git_socket_stream_new(git_stream **out, const char *host, const char *port); + +extern int git_socket_stream_global_init(void); + +#endif diff --git a/src/libgit2/streams/stransport.c b/src/libgit2/streams/stransport.c new file mode 100644 index 0000000..7a3585e --- /dev/null +++ b/src/libgit2/streams/stransport.c @@ -0,0 +1,354 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "streams/stransport.h" + +#ifdef GIT_SECURE_TRANSPORT + +#include +#include +#include + +#include "git2/transport.h" + +#include "streams/socket.h" + +static int stransport_error(OSStatus ret) +{ + CFStringRef message; + + if (ret == noErr || ret == errSSLClosedGraceful) { + git_error_clear(); + return 0; + } + +#if !TARGET_OS_IPHONE + message = SecCopyErrorMessageString(ret, NULL); + GIT_ERROR_CHECK_ALLOC(message); + + git_error_set(GIT_ERROR_NET, "SecureTransport error: %s", CFStringGetCStringPtr(message, kCFStringEncodingUTF8)); + CFRelease(message); +#else + git_error_set(GIT_ERROR_NET, "SecureTransport error: OSStatus %d", (unsigned int)ret); + GIT_UNUSED(message); +#endif + + return -1; +} + +typedef struct { + git_stream parent; + git_stream *io; + int owned; + int error; + SSLContextRef ctx; + CFDataRef der_data; + git_cert_x509 cert_info; +} stransport_stream; + +static int stransport_connect(git_stream *stream) +{ + stransport_stream *st = (stransport_stream *) stream; + int error; + SecTrustRef trust = NULL; + SecTrustResultType sec_res; + OSStatus ret; + + if (st->owned && (error = git_stream_connect(st->io)) < 0) + return error; + + ret = SSLHandshake(st->ctx); + + if (ret != errSSLServerAuthCompleted && st->error != 0) + return -1; + else if (ret != errSSLServerAuthCompleted) { + git_error_set(GIT_ERROR_SSL, "unexpected return value from ssl handshake %d", (int)ret); + return -1; + } + + if ((ret = SSLCopyPeerTrust(st->ctx, &trust)) != noErr) + goto on_error; + + if (!trust) + return GIT_ECERTIFICATE; + + if ((ret = SecTrustEvaluate(trust, &sec_res)) != noErr) + goto on_error; + + CFRelease(trust); + + if (sec_res == kSecTrustResultInvalid || sec_res == kSecTrustResultOtherError) { + git_error_set(GIT_ERROR_SSL, "internal security trust error"); + return -1; + } + + if (sec_res == kSecTrustResultDeny || sec_res == kSecTrustResultRecoverableTrustFailure || + sec_res == kSecTrustResultFatalTrustFailure) { + git_error_set(GIT_ERROR_SSL, "untrusted connection error"); + return GIT_ECERTIFICATE; + } + + return 0; + +on_error: + if (trust) + CFRelease(trust); + + return stransport_error(ret); +} + +static int stransport_certificate(git_cert **out, git_stream *stream) +{ + stransport_stream *st = (stransport_stream *) stream; + SecTrustRef trust = NULL; + SecCertificateRef sec_cert; + OSStatus ret; + + if ((ret = SSLCopyPeerTrust(st->ctx, &trust)) != noErr) + return stransport_error(ret); + + sec_cert = SecTrustGetCertificateAtIndex(trust, 0); + st->der_data = SecCertificateCopyData(sec_cert); + CFRelease(trust); + + if (st->der_data == NULL) { + git_error_set(GIT_ERROR_SSL, "retrieved invalid certificate data"); + return -1; + } + + st->cert_info.parent.cert_type = GIT_CERT_X509; + st->cert_info.data = (void *) CFDataGetBytePtr(st->der_data); + st->cert_info.len = CFDataGetLength(st->der_data); + + *out = (git_cert *)&st->cert_info; + return 0; +} + +static int stransport_set_proxy( + git_stream *stream, + const git_proxy_options *proxy_opts) +{ + stransport_stream *st = (stransport_stream *) stream; + + return git_stream_set_proxy(st->io, proxy_opts); +} + +/* + * Contrary to typical network IO callbacks, Secure Transport write callback is + * expected to write *all* passed data, not just as much as it can, and any + * other case would be considered a failure. + * + * This behavior is actually not specified in the Apple documentation, but is + * required for things to work correctly (and incidentally, that's also how + * Apple implements it in its projects at opensource.apple.com). + * + * Libgit2 streams happen to already have this very behavior so this is just + * passthrough. + */ +static OSStatus write_cb(SSLConnectionRef conn, const void *data, size_t *len) +{ + stransport_stream *st = (stransport_stream *)conn; + git_stream *io = st->io; + OSStatus ret; + + st->error = 0; + + ret = git_stream__write_full(io, data, *len, 0); + + if (ret < 0) { + st->error = ret; + return (ret == GIT_TIMEOUT) ? + -9853 /* errSSLNetworkTimeout */: + -36 /* ioErr */; + } + + return noErr; +} + +static ssize_t stransport_write(git_stream *stream, const char *data, size_t len, int flags) +{ + stransport_stream *st = (stransport_stream *) stream; + size_t data_len, processed; + OSStatus ret; + + GIT_UNUSED(flags); + + data_len = min(len, SSIZE_MAX); + if ((ret = SSLWrite(st->ctx, data, data_len, &processed)) != noErr) { + if (st->error == GIT_TIMEOUT) + return GIT_TIMEOUT; + + return stransport_error(ret); + } + + GIT_ASSERT(processed < SSIZE_MAX); + return (ssize_t)processed; +} + +/* + * Contrary to typical network IO callbacks, Secure Transport read callback is + * expected to read *exactly* the requested number of bytes, not just as much + * as it can, and any other case would be considered a failure. + * + * This behavior is actually not specified in the Apple documentation, but is + * required for things to work correctly (and incidentally, that's also how + * Apple implements it in its projects at opensource.apple.com). + */ +static OSStatus read_cb(SSLConnectionRef conn, void *data, size_t *len) +{ + stransport_stream *st = (stransport_stream *)conn; + git_stream *io = st->io; + OSStatus error = noErr; + size_t off = 0; + ssize_t ret; + + st->error = 0; + + do { + ret = git_stream_read(io, data + off, *len - off); + + if (ret < 0) { + st->error = ret; + error = (ret == GIT_TIMEOUT) ? + -9853 /* errSSLNetworkTimeout */: + -36 /* ioErr */; + break; + } else if (ret == 0) { + error = errSSLClosedGraceful; + break; + } + + off += ret; + } while (off < *len); + + *len = off; + return error; +} + +static ssize_t stransport_read(git_stream *stream, void *data, size_t len) +{ + stransport_stream *st = (stransport_stream *)stream; + size_t processed; + OSStatus ret; + + if ((ret = SSLRead(st->ctx, data, len, &processed)) != noErr) { + if (st->error == GIT_TIMEOUT) + return GIT_TIMEOUT; + + return stransport_error(ret); + } + + return processed; +} + +static int stransport_close(git_stream *stream) +{ + stransport_stream *st = (stransport_stream *) stream; + OSStatus ret; + + ret = SSLClose(st->ctx); + if (ret != noErr && ret != errSSLClosedGraceful) + return stransport_error(ret); + + return st->owned ? git_stream_close(st->io) : 0; +} + +static void stransport_free(git_stream *stream) +{ + stransport_stream *st = (stransport_stream *) stream; + + if (st->owned) + git_stream_free(st->io); + + CFRelease(st->ctx); + if (st->der_data) + CFRelease(st->der_data); + git__free(st); +} + +static int stransport_wrap( + git_stream **out, + git_stream *in, + const char *host, + int owned) +{ + stransport_stream *st; + OSStatus ret; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(in); + GIT_ASSERT_ARG(host); + + st = git__calloc(1, sizeof(stransport_stream)); + GIT_ERROR_CHECK_ALLOC(st); + + st->io = in; + st->owned = owned; + + st->ctx = SSLCreateContext(NULL, kSSLClientSide, kSSLStreamType); + if (!st->ctx) { + git_error_set(GIT_ERROR_NET, "failed to create SSL context"); + git__free(st); + return -1; + } + + if ((ret = SSLSetIOFuncs(st->ctx, read_cb, write_cb)) != noErr || + (ret = SSLSetConnection(st->ctx, st)) != noErr || + (ret = SSLSetSessionOption(st->ctx, kSSLSessionOptionBreakOnServerAuth, true)) != noErr || + (ret = SSLSetProtocolVersionMin(st->ctx, kTLSProtocol1)) != noErr || + (ret = SSLSetProtocolVersionMax(st->ctx, kTLSProtocol12)) != noErr || + (ret = SSLSetPeerDomainName(st->ctx, host, strlen(host))) != noErr) { + CFRelease(st->ctx); + git__free(st); + return stransport_error(ret); + } + + st->parent.version = GIT_STREAM_VERSION; + st->parent.encrypted = 1; + st->parent.proxy_support = git_stream_supports_proxy(st->io); + st->parent.connect = stransport_connect; + st->parent.certificate = stransport_certificate; + st->parent.set_proxy = stransport_set_proxy; + st->parent.read = stransport_read; + st->parent.write = stransport_write; + st->parent.close = stransport_close; + st->parent.free = stransport_free; + + *out = (git_stream *) st; + return 0; +} + +int git_stransport_stream_wrap( + git_stream **out, + git_stream *in, + const char *host) +{ + return stransport_wrap(out, in, host, 0); +} + +int git_stransport_stream_new(git_stream **out, const char *host, const char *port) +{ + git_stream *stream = NULL; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(host); + + error = git_socket_stream_new(&stream, host, port); + + if (!error) + error = stransport_wrap(out, stream, host, 1); + + if (error < 0 && stream) { + git_stream_close(stream); + git_stream_free(stream); + } + + return error; +} + +#endif diff --git a/src/libgit2/streams/stransport.h b/src/libgit2/streams/stransport.h new file mode 100644 index 0000000..1026e20 --- /dev/null +++ b/src/libgit2/streams/stransport.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_streams_stransport_h__ +#define INCLUDE_streams_stransport_h__ + +#include "common.h" + +#include "git2/sys/stream.h" + +#ifdef GIT_SECURE_TRANSPORT + +extern int git_stransport_stream_new(git_stream **out, const char *host, const char *port); +extern int git_stransport_stream_wrap(git_stream **out, git_stream *in, const char *host); + +#endif + +#endif diff --git a/src/libgit2/streams/tls.c b/src/libgit2/streams/tls.c new file mode 100644 index 0000000..246ac9c --- /dev/null +++ b/src/libgit2/streams/tls.c @@ -0,0 +1,80 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2/errors.h" + +#include "common.h" +#include "streams/registry.h" +#include "streams/tls.h" +#include "streams/mbedtls.h" +#include "streams/openssl.h" +#include "streams/stransport.h" +#include "streams/schannel.h" + +int git_tls_stream_new(git_stream **out, const char *host, const char *port) +{ + int (*init)(git_stream **, const char *, const char *) = NULL; + git_stream_registration custom = {0}; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(host); + GIT_ASSERT_ARG(port); + + if ((error = git_stream_registry_lookup(&custom, GIT_STREAM_TLS)) == 0) { + init = custom.init; + } else if (error == GIT_ENOTFOUND) { +#ifdef GIT_SECURE_TRANSPORT + init = git_stransport_stream_new; +#elif defined(GIT_OPENSSL) + init = git_openssl_stream_new; +#elif defined(GIT_MBEDTLS) + init = git_mbedtls_stream_new; +#elif defined(GIT_SCHANNEL) + init = git_schannel_stream_new; +#endif + } else { + return error; + } + + if (!init) { + git_error_set(GIT_ERROR_SSL, "there is no TLS stream available"); + return -1; + } + + return init(out, host, port); +} + +int git_tls_stream_wrap(git_stream **out, git_stream *in, const char *host) +{ + int (*wrap)(git_stream **, git_stream *, const char *) = NULL; + git_stream_registration custom = {0}; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(in); + + if (git_stream_registry_lookup(&custom, GIT_STREAM_TLS) == 0) { + wrap = custom.wrap; + } else { +#ifdef GIT_SECURE_TRANSPORT + wrap = git_stransport_stream_wrap; +#elif defined(GIT_OPENSSL) + wrap = git_openssl_stream_wrap; +#elif defined(GIT_MBEDTLS) + wrap = git_mbedtls_stream_wrap; +#elif defined(GIT_SCHANNEL) + wrap = git_schannel_stream_wrap; +#endif + } + + if (!wrap) { + git_error_set(GIT_ERROR_SSL, "there is no TLS stream available"); + return -1; + } + + return wrap(out, in, host); +} diff --git a/src/libgit2/streams/tls.h b/src/libgit2/streams/tls.h new file mode 100644 index 0000000..465a6ea --- /dev/null +++ b/src/libgit2/streams/tls.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_streams_tls_h__ +#define INCLUDE_streams_tls_h__ + +#include "common.h" + +#include "git2/sys/stream.h" + +/** + * Create a TLS stream with the most appropriate backend available for + * the current platform, whether that's SecureTransport on macOS, + * OpenSSL or mbedTLS on other Unixes, or something else entirely. + */ +extern int git_tls_stream_new(git_stream **out, const char *host, const char *port); + +/** + * Create a TLS stream on top of an existing insecure stream, using + * the most appropriate backend available for the current platform. + * + * This allows us to create a CONNECT stream on top of a proxy; + * using SecureTransport on macOS, OpenSSL or mbedTLS on other + * Unixes, or something else entirely. + */ +extern int git_tls_stream_wrap(git_stream **out, git_stream *in, const char *host); + +#endif diff --git a/src/libgit2/submodule.c b/src/libgit2/submodule.c new file mode 100644 index 0000000..95ea84f --- /dev/null +++ b/src/libgit2/submodule.c @@ -0,0 +1,2384 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "submodule.h" + +#include "buf.h" +#include "branch.h" +#include "vector.h" +#include "posix.h" +#include "config_backend.h" +#include "config.h" +#include "repository.h" +#include "tree.h" +#include "iterator.h" +#include "fs_path.h" +#include "str.h" +#include "index.h" +#include "worktree.h" +#include "clone.h" +#include "path.h" + +#include "git2/config.h" +#include "git2/sys/config.h" +#include "git2/types.h" +#include "git2/index.h" + +#define GIT_MODULES_FILE ".gitmodules" + +static git_configmap _sm_update_map[] = { + {GIT_CONFIGMAP_STRING, "checkout", GIT_SUBMODULE_UPDATE_CHECKOUT}, + {GIT_CONFIGMAP_STRING, "rebase", GIT_SUBMODULE_UPDATE_REBASE}, + {GIT_CONFIGMAP_STRING, "merge", GIT_SUBMODULE_UPDATE_MERGE}, + {GIT_CONFIGMAP_STRING, "none", GIT_SUBMODULE_UPDATE_NONE}, + {GIT_CONFIGMAP_FALSE, NULL, GIT_SUBMODULE_UPDATE_NONE}, + {GIT_CONFIGMAP_TRUE, NULL, GIT_SUBMODULE_UPDATE_CHECKOUT}, +}; + +static git_configmap _sm_ignore_map[] = { + {GIT_CONFIGMAP_STRING, "none", GIT_SUBMODULE_IGNORE_NONE}, + {GIT_CONFIGMAP_STRING, "untracked", GIT_SUBMODULE_IGNORE_UNTRACKED}, + {GIT_CONFIGMAP_STRING, "dirty", GIT_SUBMODULE_IGNORE_DIRTY}, + {GIT_CONFIGMAP_STRING, "all", GIT_SUBMODULE_IGNORE_ALL}, + {GIT_CONFIGMAP_FALSE, NULL, GIT_SUBMODULE_IGNORE_NONE}, + {GIT_CONFIGMAP_TRUE, NULL, GIT_SUBMODULE_IGNORE_ALL}, +}; + +static git_configmap _sm_recurse_map[] = { + {GIT_CONFIGMAP_STRING, "on-demand", GIT_SUBMODULE_RECURSE_ONDEMAND}, + {GIT_CONFIGMAP_FALSE, NULL, GIT_SUBMODULE_RECURSE_NO}, + {GIT_CONFIGMAP_TRUE, NULL, GIT_SUBMODULE_RECURSE_YES}, +}; + +enum { + CACHE_OK = 0, + CACHE_REFRESH = 1, + CACHE_FLUSH = 2 +}; +enum { + GITMODULES_EXISTING = 0, + GITMODULES_CREATE = 1 +}; + +static int submodule_alloc(git_submodule **out, git_repository *repo, const char *name); +static git_config_backend *open_gitmodules(git_repository *repo, int gitmod); +static int gitmodules_snapshot(git_config **snap, git_repository *repo); +static int get_url_base(git_str *url, git_repository *repo); +static int lookup_head_remote_key(git_str *remote_key, git_repository *repo); +static int lookup_default_remote(git_remote **remote, git_repository *repo); +static int submodule_load_each(const git_config_entry *entry, void *payload); +static int submodule_read_config(git_submodule *sm, git_config *cfg); +static int submodule_load_from_wd_lite(git_submodule *); +static void submodule_get_index_status(unsigned int *, git_submodule *); +static void submodule_get_wd_status(unsigned int *, git_submodule *, git_repository *, git_submodule_ignore_t); +static void submodule_update_from_index_entry(git_submodule *sm, const git_index_entry *ie); +static void submodule_update_from_head_data(git_submodule *sm, mode_t mode, const git_oid *id); + +static int submodule_cmp(const void *a, const void *b) +{ + return strcmp(((git_submodule *)a)->name, ((git_submodule *)b)->name); +} + +static int submodule_config_key_trunc_puts(git_str *key, const char *suffix) +{ + ssize_t idx = git_str_rfind(key, '.'); + git_str_truncate(key, (size_t)(idx + 1)); + return git_str_puts(key, suffix); +} + +/* + * PUBLIC APIS + */ + +static void submodule_set_lookup_error(int error, const char *name) +{ + if (!error) + return; + + git_error_set(GIT_ERROR_SUBMODULE, (error == GIT_ENOTFOUND) ? + "no submodule named '%s'" : + "submodule '%s' has not been added yet", name); +} + +typedef struct { + const char *path; + char *name; +} fbp_data; + +static int find_by_path(const git_config_entry *entry, void *payload) +{ + fbp_data *data = payload; + + if (!strcmp(entry->value, data->path)) { + const char *fdot, *ldot; + fdot = strchr(entry->name, '.'); + ldot = strrchr(entry->name, '.'); + data->name = git__strndup(fdot + 1, ldot - fdot - 1); + GIT_ERROR_CHECK_ALLOC(data->name); + } + + return 0; +} + +/* + * Checks to see if the submodule shares its name with a file or directory that + * already exists on the index. If so, the submodule cannot be added. + */ +static int is_path_occupied(bool *occupied, git_repository *repo, const char *path) +{ + int error = 0; + git_index *index; + git_str dir = GIT_STR_INIT; + *occupied = false; + + if ((error = git_repository_index__weakptr(&index, repo)) < 0) + goto out; + + if ((error = git_index_find(NULL, index, path)) != GIT_ENOTFOUND) { + if (!error) { + git_error_set(GIT_ERROR_SUBMODULE, + "File '%s' already exists in the index", path); + *occupied = true; + } + goto out; + } + + if ((error = git_str_sets(&dir, path)) < 0) + goto out; + + if ((error = git_fs_path_to_dir(&dir)) < 0) + goto out; + + if ((error = git_index_find_prefix(NULL, index, dir.ptr)) != GIT_ENOTFOUND) { + if (!error) { + git_error_set(GIT_ERROR_SUBMODULE, + "Directory '%s' already exists in the index", path); + *occupied = true; + } + goto out; + } + + error = 0; + +out: + git_str_dispose(&dir); + return error; +} + +/** + * Release the name map returned by 'load_submodule_names'. + */ +static void free_submodule_names(git_strmap *names) +{ + const char *key; + char *value; + + if (names == NULL) + return; + + git_strmap_foreach(names, key, value, { + git__free((char *) key); + git__free(value); + }); + git_strmap_free(names); + + return; +} + +/** + * Map submodule paths to names. + * TODO: for some use-cases, this might need case-folding on a + * case-insensitive filesystem + */ +static int load_submodule_names(git_strmap **out, git_repository *repo, git_config *cfg) +{ + const char *key = "submodule\\..*\\.path"; + git_config_iterator *iter = NULL; + git_config_entry *entry; + git_str buf = GIT_STR_INIT; + git_strmap *names; + int isvalid, error; + + *out = NULL; + + if ((error = git_strmap_new(&names)) < 0) + goto out; + + if ((error = git_config_iterator_glob_new(&iter, cfg, key)) < 0) + goto out; + + while ((error = git_config_next(&entry, iter)) == 0) { + const char *fdot, *ldot; + fdot = strchr(entry->name, '.'); + ldot = strrchr(entry->name, '.'); + + if (git_strmap_exists(names, entry->value)) { + git_error_set(GIT_ERROR_SUBMODULE, + "duplicated submodule path '%s'", entry->value); + error = -1; + goto out; + } + + git_str_clear(&buf); + git_str_put(&buf, fdot + 1, ldot - fdot - 1); + isvalid = git_submodule_name_is_valid(repo, buf.ptr, 0); + if (isvalid < 0) { + error = isvalid; + goto out; + } + if (!isvalid) + continue; + + if ((error = git_strmap_set(names, git__strdup(entry->value), git_str_detach(&buf))) < 0) { + git_error_set(GIT_ERROR_NOMEMORY, "error inserting submodule into hash table"); + error = -1; + goto out; + } + } + if (error == GIT_ITEROVER) + error = 0; + + *out = names; + names = NULL; + +out: + free_submodule_names(names); + git_str_dispose(&buf); + git_config_iterator_free(iter); + return error; +} + +int git_submodule_cache_init(git_strmap **out, git_repository *repo) +{ + int error = 0; + git_strmap *cache = NULL; + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + if ((error = git_strmap_new(&cache)) < 0) + return error; + if ((error = git_submodule__map(repo, cache)) < 0) { + git_submodule_cache_free(cache); + return error; + } + *out = cache; + return error; +} + +int git_submodule_cache_free(git_strmap *cache) +{ + git_submodule *sm = NULL; + if (cache == NULL) + return 0; + git_strmap_foreach_value(cache, sm, { + git_submodule_free(sm); + }); + git_strmap_free(cache); + return 0; +} + +int git_submodule_lookup( + git_submodule **out, /* NULL if user only wants to test existence */ + git_repository *repo, + const char *name) /* trailing slash is allowed */ +{ + return git_submodule__lookup_with_cache(out, repo, name, repo->submodule_cache); +} + +int git_submodule__lookup_with_cache( + git_submodule **out, /* NULL if user only wants to test existence */ + git_repository *repo, + const char *name, /* trailing slash is allowed */ + git_strmap *cache) +{ + int error; + unsigned int location; + git_submodule *sm; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + if (repo->is_bare) { + git_error_set(GIT_ERROR_SUBMODULE, "cannot get submodules without a working tree"); + return -1; + } + + if (cache != NULL) { + if ((sm = git_strmap_get(cache, name)) != NULL) { + if (out) { + *out = sm; + GIT_REFCOUNT_INC(*out); + } + return 0; + } + } + + if ((error = submodule_alloc(&sm, repo, name)) < 0) + return error; + + if ((error = git_submodule_reload(sm, false)) < 0) { + git_submodule_free(sm); + return error; + } + + if ((error = git_submodule_location(&location, sm)) < 0) { + git_submodule_free(sm); + return error; + } + + /* If it's not configured or we're looking by path */ + if (location == 0 || location == GIT_SUBMODULE_STATUS_IN_WD) { + git_config_backend *mods; + const char *pattern = "submodule\\..*\\.path"; + git_str path = GIT_STR_INIT; + fbp_data data = { NULL, NULL }; + + git_str_puts(&path, name); + while (path.ptr[path.size-1] == '/') { + path.ptr[--path.size] = '\0'; + } + data.path = path.ptr; + + mods = open_gitmodules(repo, GITMODULES_EXISTING); + + if (mods) + error = git_config_backend_foreach_match(mods, pattern, find_by_path, &data); + + git_config_backend_free(mods); + + if (error < 0) { + git_submodule_free(sm); + git_str_dispose(&path); + return error; + } + + if (data.name) { + git__free(sm->name); + sm->name = data.name; + sm->path = git_str_detach(&path); + + /* Try to load again with the right name */ + if ((error = git_submodule_reload(sm, false)) < 0) { + git_submodule_free(sm); + return error; + } + } + + git_str_dispose(&path); + } + + if ((error = git_submodule_location(&location, sm)) < 0) { + git_submodule_free(sm); + return error; + } + + /* If we still haven't found it, do the WD check */ + if (location == 0 || location == GIT_SUBMODULE_STATUS_IN_WD) { + git_submodule_free(sm); + error = GIT_ENOTFOUND; + + /* If it's not configured, we still check if there's a repo at the path */ + if (git_repository_workdir(repo)) { + git_str path = GIT_STR_INIT; + if (git_str_join3(&path, '/', + git_repository_workdir(repo), + name, DOT_GIT) < 0 || + git_path_validate_str_length(NULL, &path) < 0) + return -1; + + if (git_fs_path_exists(path.ptr)) + error = GIT_EEXISTS; + + git_str_dispose(&path); + } + + submodule_set_lookup_error(error, name); + return error; + } + + if (out) + *out = sm; + else + git_submodule_free(sm); + + return 0; +} + +int git_submodule_name_is_valid(git_repository *repo, const char *name, int flags) +{ + git_str buf = GIT_STR_INIT; + int error, isvalid; + + if (flags == 0) + flags = GIT_FS_PATH_REJECT_FILESYSTEM_DEFAULTS; + + /* Avoid allocating a new string if we can avoid it */ + if (strchr(name, '\\') != NULL) { + if ((error = git_fs_path_normalize_slashes(&buf, name)) < 0) + return error; + } else { + git_str_attach_notowned(&buf, name, strlen(name)); + } + + isvalid = git_path_is_valid(repo, buf.ptr, 0, flags); + git_str_dispose(&buf); + + return isvalid; +} + +static void submodule_free_dup(void *sm) +{ + git_submodule_free(sm); +} + +static int submodule_get_or_create(git_submodule **out, git_repository *repo, git_strmap *map, const char *name) +{ + git_submodule *sm = NULL; + int error; + + if ((sm = git_strmap_get(map, name)) != NULL) + goto done; + + /* if the submodule doesn't exist yet in the map, create it */ + if ((error = submodule_alloc(&sm, repo, name)) < 0) + return error; + + if ((error = git_strmap_set(map, sm->name, sm)) < 0) { + git_submodule_free(sm); + return error; + } + +done: + GIT_REFCOUNT_INC(sm); + *out = sm; + return 0; +} + +static int submodules_from_index(git_strmap *map, git_index *idx, git_config *cfg) +{ + int error; + git_iterator *i = NULL; + const git_index_entry *entry; + git_strmap *names; + + if ((error = load_submodule_names(&names, git_index_owner(idx), cfg))) + goto done; + + if ((error = git_iterator_for_index(&i, git_index_owner(idx), idx, NULL)) < 0) + goto done; + + while (!(error = git_iterator_advance(&entry, i))) { + git_submodule *sm; + + if ((sm = git_strmap_get(map, entry->path)) != NULL) { + if (S_ISGITLINK(entry->mode)) + submodule_update_from_index_entry(sm, entry); + else + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE; + } else if (S_ISGITLINK(entry->mode)) { + const char *name; + + if ((name = git_strmap_get(names, entry->path)) == NULL) + name = entry->path; + + if (!submodule_get_or_create(&sm, git_index_owner(idx), map, name)) { + submodule_update_from_index_entry(sm, entry); + git_submodule_free(sm); + } + } + } + + if (error == GIT_ITEROVER) + error = 0; + +done: + git_iterator_free(i); + free_submodule_names(names); + + return error; +} + +static int submodules_from_head(git_strmap *map, git_tree *head, git_config *cfg) +{ + int error; + git_iterator *i = NULL; + const git_index_entry *entry; + git_strmap *names; + + if ((error = load_submodule_names(&names, git_tree_owner(head), cfg))) + goto done; + + if ((error = git_iterator_for_tree(&i, head, NULL)) < 0) + goto done; + + while (!(error = git_iterator_advance(&entry, i))) { + git_submodule *sm; + + if ((sm = git_strmap_get(map, entry->path)) != NULL) { + if (S_ISGITLINK(entry->mode)) + submodule_update_from_head_data(sm, entry->mode, &entry->id); + else + sm->flags |= GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE; + } else if (S_ISGITLINK(entry->mode)) { + const char *name; + + if ((name = git_strmap_get(names, entry->path)) == NULL) + name = entry->path; + + if (!submodule_get_or_create(&sm, git_tree_owner(head), map, name)) { + submodule_update_from_head_data( + sm, entry->mode, &entry->id); + git_submodule_free(sm); + } + } + } + + if (error == GIT_ITEROVER) + error = 0; + +done: + git_iterator_free(i); + free_submodule_names(names); + + return error; +} + +/* If have_sm is true, sm is populated, otherwise map an repo are. */ +typedef struct { + git_config *mods; + git_strmap *map; + git_repository *repo; +} lfc_data; + +int git_submodule__map(git_repository *repo, git_strmap *map) +{ + int error = 0; + git_index *idx = NULL; + git_tree *head = NULL; + git_str path = GIT_STR_INIT; + git_submodule *sm; + git_config *mods = NULL; + bool has_workdir; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(map); + + /* get sources that we will need to check */ + if (git_repository_index(&idx, repo) < 0) + git_error_clear(); + if (git_repository_head_tree(&head, repo) < 0) + git_error_clear(); + + has_workdir = git_repository_workdir(repo) != NULL; + + if (has_workdir && + (error = git_repository_workdir_path(&path, repo, GIT_MODULES_FILE)) < 0) + goto cleanup; + + /* add submodule information from .gitmodules */ + if (has_workdir) { + lfc_data data = { 0 }; + data.map = map; + data.repo = repo; + + if ((error = gitmodules_snapshot(&mods, repo)) < 0) { + if (error == GIT_ENOTFOUND) + error = 0; + goto cleanup; + } + + data.mods = mods; + if ((error = git_config_foreach( + mods, submodule_load_each, &data)) < 0) + goto cleanup; + } + /* add back submodule information from index */ + if (mods && idx) { + if ((error = submodules_from_index(map, idx, mods)) < 0) + goto cleanup; + } + /* add submodule information from HEAD */ + if (mods && head) { + if ((error = submodules_from_head(map, head, mods)) < 0) + goto cleanup; + } + /* shallow scan submodules in work tree as needed */ + if (has_workdir) { + git_strmap_foreach_value(map, sm, { + submodule_load_from_wd_lite(sm); + }); + } + +cleanup: + git_config_free(mods); + /* TODO: if we got an error, mark submodule config as invalid? */ + git_index_free(idx); + git_tree_free(head); + git_str_dispose(&path); + return error; +} + +int git_submodule_foreach( + git_repository *repo, + git_submodule_cb callback, + void *payload) +{ + git_vector snapshot = GIT_VECTOR_INIT; + git_strmap *submodules; + git_submodule *sm; + int error; + size_t i; + + if (repo->is_bare) { + git_error_set(GIT_ERROR_SUBMODULE, "cannot get submodules without a working tree"); + return -1; + } + + if ((error = git_strmap_new(&submodules)) < 0) + return error; + + if ((error = git_submodule__map(repo, submodules)) < 0) + goto done; + + if (!(error = git_vector_init( + &snapshot, git_strmap_size(submodules), submodule_cmp))) { + + git_strmap_foreach_value(submodules, sm, { + if ((error = git_vector_insert(&snapshot, sm)) < 0) + break; + GIT_REFCOUNT_INC(sm); + }); + } + + if (error < 0) + goto done; + + git_vector_uniq(&snapshot, submodule_free_dup); + + git_vector_foreach(&snapshot, i, sm) { + if ((error = callback(sm, sm->name, payload)) != 0) { + git_error_set_after_callback(error); + break; + } + } + +done: + git_vector_foreach(&snapshot, i, sm) + git_submodule_free(sm); + git_vector_free(&snapshot); + + git_strmap_foreach_value(submodules, sm, { + git_submodule_free(sm); + }); + git_strmap_free(submodules); + + return error; +} + +static int submodule_repo_init( + git_repository **out, + git_repository *parent_repo, + const char *path, + const char *url, + bool use_gitlink) +{ + int error = 0; + git_str workdir = GIT_STR_INIT, repodir = GIT_STR_INIT; + git_repository_init_options initopt = GIT_REPOSITORY_INIT_OPTIONS_INIT; + git_repository *subrepo = NULL; + + error = git_repository_workdir_path(&workdir, parent_repo, path); + if (error < 0) + goto cleanup; + + initopt.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_NO_REINIT; + initopt.origin_url = url; + + /* init submodule repository and add origin remote as needed */ + + /* New style: sub-repo goes in /modules// with a + * gitlink in the sub-repo workdir directory to that repository + * + * Old style: sub-repo goes directly into repo//.git/ + */ + if (use_gitlink) { + error = git_repository__item_path(&repodir, parent_repo, GIT_REPOSITORY_ITEM_MODULES); + if (error < 0) + goto cleanup; + error = git_str_joinpath(&repodir, repodir.ptr, path); + if (error < 0) + goto cleanup; + + initopt.workdir_path = workdir.ptr; + initopt.flags |= + GIT_REPOSITORY_INIT_NO_DOTGIT_DIR | + GIT_REPOSITORY_INIT_RELATIVE_GITLINK; + + error = git_repository_init_ext(&subrepo, repodir.ptr, &initopt); + } else + error = git_repository_init_ext(&subrepo, workdir.ptr, &initopt); + +cleanup: + git_str_dispose(&workdir); + git_str_dispose(&repodir); + + *out = subrepo; + + return error; +} + +static int git_submodule__resolve_url( + git_str *out, + git_repository *repo, + const char *url) +{ + int error = 0; + git_str normalized = GIT_STR_INIT; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(url); + + /* We do this in all platforms in case someone on Windows created the .gitmodules */ + if (strchr(url, '\\')) { + if ((error = git_fs_path_normalize_slashes(&normalized, url)) < 0) + return error; + + url = normalized.ptr; + } + + + if (git_fs_path_is_relative(url)) { + if (!(error = get_url_base(out, repo))) + error = git_fs_path_apply_relative(out, url); + } else if (strchr(url, ':') != NULL || url[0] == '/') { + error = git_str_sets(out, url); + } else { + git_error_set(GIT_ERROR_SUBMODULE, "invalid format for submodule URL"); + error = -1; + } + + git_str_dispose(&normalized); + return error; +} + +int git_submodule_resolve_url( + git_buf *out, + git_repository *repo, + const char *url) +{ + GIT_BUF_WRAP_PRIVATE(out, git_submodule__resolve_url, repo, url); +} + +int git_submodule_add_setup( + git_submodule **out, + git_repository *repo, + const char *url, + const char *path, + int use_gitlink) +{ + int error = 0; + git_config_backend *mods = NULL; + git_submodule *sm = NULL; + git_str name = GIT_STR_INIT, real_url = GIT_STR_INIT; + git_repository *subrepo = NULL; + bool path_occupied; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(url); + GIT_ASSERT_ARG(path); + + /* see if there is already an entry for this submodule */ + + if (git_submodule_lookup(NULL, repo, path) < 0) + git_error_clear(); + else { + git_error_set(GIT_ERROR_SUBMODULE, + "attempt to add submodule '%s' that already exists", path); + return GIT_EEXISTS; + } + + /* validate and normalize path */ + + if (git__prefixcmp(path, git_repository_workdir(repo)) == 0) + path += strlen(git_repository_workdir(repo)); + + if (git_fs_path_root(path) >= 0) { + git_error_set(GIT_ERROR_SUBMODULE, "submodule path must be a relative path"); + error = -1; + goto cleanup; + } + + if ((error = is_path_occupied(&path_occupied, repo, path)) < 0) + goto cleanup; + + if (path_occupied) { + error = GIT_EEXISTS; + goto cleanup; + } + + /* update .gitmodules */ + + if (!(mods = open_gitmodules(repo, GITMODULES_CREATE))) { + git_error_set(GIT_ERROR_SUBMODULE, + "adding submodules to a bare repository is not supported"); + return -1; + } + + if ((error = git_str_printf(&name, "submodule.%s.path", path)) < 0 || + (error = git_config_backend_set_string(mods, name.ptr, path)) < 0) + goto cleanup; + + if ((error = submodule_config_key_trunc_puts(&name, "url")) < 0 || + (error = git_config_backend_set_string(mods, name.ptr, url)) < 0) + goto cleanup; + + git_str_clear(&name); + + /* init submodule repository and add origin remote as needed */ + + error = git_repository_workdir_path(&name, repo, path); + if (error < 0) + goto cleanup; + + /* if the repo does not already exist, then init a new repo and add it. + * Otherwise, just add the existing repo. + */ + if (!(git_fs_path_exists(name.ptr) && + git_fs_path_contains(&name, DOT_GIT))) { + + /* resolve the actual URL to use */ + if ((error = git_submodule__resolve_url(&real_url, repo, url)) < 0) + goto cleanup; + + if ((error = submodule_repo_init(&subrepo, repo, path, real_url.ptr, use_gitlink)) < 0) + goto cleanup; + } + + if ((error = git_submodule_lookup(&sm, repo, path)) < 0) + goto cleanup; + + error = git_submodule_init(sm, false); + +cleanup: + if (error && sm) { + git_submodule_free(sm); + sm = NULL; + } + if (out != NULL) + *out = sm; + + git_config_backend_free(mods); + git_repository_free(subrepo); + git_str_dispose(&real_url); + git_str_dispose(&name); + + return error; +} + +int git_submodule_repo_init( + git_repository **out, + const git_submodule *sm, + int use_gitlink) +{ + int error; + git_repository *sub_repo = NULL; + const char *configured_url; + git_config *cfg = NULL; + git_str buf = GIT_STR_INIT; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(sm); + + /* get the configured remote url of the submodule */ + if ((error = git_str_printf(&buf, "submodule.%s.url", sm->name)) < 0 || + (error = git_repository_config_snapshot(&cfg, sm->repo)) < 0 || + (error = git_config_get_string(&configured_url, cfg, buf.ptr)) < 0 || + (error = submodule_repo_init(&sub_repo, sm->repo, sm->path, configured_url, use_gitlink)) < 0) + goto done; + + *out = sub_repo; + +done: + git_config_free(cfg); + git_str_dispose(&buf); + return error; +} + +static int clone_return_origin(git_remote **out, git_repository *repo, const char *name, const char *url, void *payload) +{ + GIT_UNUSED(url); + GIT_UNUSED(payload); + return git_remote_lookup(out, repo, name); +} + +static int clone_return_repo(git_repository **out, const char *path, int bare, void *payload) +{ + git_submodule *sm = payload; + + GIT_UNUSED(path); + GIT_UNUSED(bare); + return git_submodule_open(out, sm); +} + +int git_submodule_clone(git_repository **out, git_submodule *submodule, const git_submodule_update_options *given_opts) +{ + int error; + git_repository *clone; + git_str rel_path = GIT_STR_INIT; + git_submodule_update_options sub_opts = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + git_clone_options opts = GIT_CLONE_OPTIONS_INIT; + + GIT_ASSERT_ARG(submodule); + + if (given_opts) + memcpy(&sub_opts, given_opts, sizeof(sub_opts)); + + GIT_ERROR_CHECK_VERSION(&sub_opts, GIT_SUBMODULE_UPDATE_OPTIONS_VERSION, "git_submodule_update_options"); + + memcpy(&opts.checkout_opts, &sub_opts.checkout_opts, sizeof(sub_opts.checkout_opts)); + memcpy(&opts.fetch_opts, &sub_opts.fetch_opts, sizeof(sub_opts.fetch_opts)); + opts.repository_cb = clone_return_repo; + opts.repository_cb_payload = submodule; + opts.remote_cb = clone_return_origin; + opts.remote_cb_payload = submodule; + + error = git_repository_workdir_path(&rel_path, git_submodule_owner(submodule), git_submodule_path(submodule)); + if (error < 0) + goto cleanup; + + error = git_clone__submodule(&clone, git_submodule_url(submodule), git_str_cstr(&rel_path), &opts); + if (error < 0) + goto cleanup; + + if (!out) + git_repository_free(clone); + else + *out = clone; + +cleanup: + git_str_dispose(&rel_path); + + return error; +} + +int git_submodule_add_finalize(git_submodule *sm) +{ + int error; + git_index *index; + + GIT_ASSERT_ARG(sm); + + if ((error = git_repository_index__weakptr(&index, sm->repo)) < 0 || + (error = git_index_add_bypath(index, GIT_MODULES_FILE)) < 0) + return error; + + return git_submodule_add_to_index(sm, true); +} + +int git_submodule_add_to_index(git_submodule *sm, int write_index) +{ + int error; + git_repository *sm_repo = NULL; + git_index *index; + git_str path = GIT_STR_INIT; + git_commit *head; + git_index_entry entry; + struct stat st; + + GIT_ASSERT_ARG(sm); + + /* force reload of wd OID by git_submodule_open */ + sm->flags = sm->flags & ~GIT_SUBMODULE_STATUS__WD_OID_VALID; + + if ((error = git_repository_index__weakptr(&index, sm->repo)) < 0 || + (error = git_repository_workdir_path(&path, sm->repo, sm->path)) < 0 || + (error = git_submodule_open(&sm_repo, sm)) < 0) + goto cleanup; + + /* read stat information for submodule working directory */ + if (p_stat(path.ptr, &st) < 0) { + git_error_set(GIT_ERROR_SUBMODULE, + "cannot add submodule without working directory"); + error = -1; + goto cleanup; + } + + memset(&entry, 0, sizeof(entry)); + entry.path = sm->path; + git_index_entry__init_from_stat( + &entry, &st, !(git_index_caps(index) & GIT_INDEX_CAPABILITY_NO_FILEMODE)); + + /* calling git_submodule_open will have set sm->wd_oid if possible */ + if ((sm->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) == 0) { + git_error_set(GIT_ERROR_SUBMODULE, + "cannot add submodule without HEAD to index"); + error = -1; + goto cleanup; + } + git_oid_cpy(&entry.id, &sm->wd_oid); + + if ((error = git_commit_lookup(&head, sm_repo, &sm->wd_oid)) < 0) + goto cleanup; + + entry.ctime.seconds = (int32_t)git_commit_time(head); + entry.ctime.nanoseconds = 0; + entry.mtime.seconds = (int32_t)git_commit_time(head); + entry.mtime.nanoseconds = 0; + + git_commit_free(head); + + /* add it */ + error = git_index_add(index, &entry); + + /* write it, if requested */ + if (!error && write_index) { + error = git_index_write(index); + + if (!error) + git_oid_cpy(&sm->index_oid, &sm->wd_oid); + } + +cleanup: + git_repository_free(sm_repo); + git_str_dispose(&path); + return error; +} + +static const char *submodule_update_to_str(git_submodule_update_t update) +{ + int i; + for (i = 0; i < (int)ARRAY_SIZE(_sm_update_map); ++i) + if (_sm_update_map[i].map_value == (int)update) + return _sm_update_map[i].str_match; + return NULL; +} + +git_repository *git_submodule_owner(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + return submodule->repo; +} + +const char *git_submodule_name(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + return submodule->name; +} + +const char *git_submodule_path(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + return submodule->path; +} + +const char *git_submodule_url(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + return submodule->url; +} + +static int write_var(git_repository *repo, const char *name, const char *var, const char *val) +{ + git_str key = GIT_STR_INIT; + git_config_backend *mods; + int error; + + mods = open_gitmodules(repo, GITMODULES_CREATE); + if (!mods) + return -1; + + if ((error = git_str_printf(&key, "submodule.%s.%s", name, var)) < 0) + goto cleanup; + + if (val) + error = git_config_backend_set_string(mods, key.ptr, val); + else + error = git_config_backend_delete(mods, key.ptr); + + git_str_dispose(&key); + +cleanup: + git_config_backend_free(mods); + return error; +} + +static int write_mapped_var(git_repository *repo, const char *name, git_configmap *maps, size_t nmaps, const char *var, int ival) +{ + git_configmap_t type; + const char *val; + + if (git_config_lookup_map_enum(&type, &val, maps, nmaps, ival) < 0) { + git_error_set(GIT_ERROR_SUBMODULE, "invalid value for %s", var); + return -1; + } + + if (type == GIT_CONFIGMAP_TRUE) + val = "true"; + + return write_var(repo, name, var, val); +} + +const char *git_submodule_branch(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + return submodule->branch; +} + +int git_submodule_set_branch(git_repository *repo, const char *name, const char *branch) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + return write_var(repo, name, "branch", branch); +} + +int git_submodule_set_url(git_repository *repo, const char *name, const char *url) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + GIT_ASSERT_ARG(url); + + return write_var(repo, name, "url", url); +} + +const git_oid *git_submodule_index_id(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + + if (submodule->flags & GIT_SUBMODULE_STATUS__INDEX_OID_VALID) + return &submodule->index_oid; + else + return NULL; +} + +const git_oid *git_submodule_head_id(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + + if (submodule->flags & GIT_SUBMODULE_STATUS__HEAD_OID_VALID) + return &submodule->head_oid; + else + return NULL; +} + +const git_oid *git_submodule_wd_id(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, NULL); + + /* load unless we think we have a valid oid */ + if (!(submodule->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID)) { + git_repository *subrepo; + + /* calling submodule open grabs the HEAD OID if possible */ + if (!git_submodule_open_bare(&subrepo, submodule)) + git_repository_free(subrepo); + else + git_error_clear(); + } + + if (submodule->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) + return &submodule->wd_oid; + else + return NULL; +} + +git_submodule_ignore_t git_submodule_ignore(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, GIT_SUBMODULE_IGNORE_UNSPECIFIED); + + return (submodule->ignore < GIT_SUBMODULE_IGNORE_NONE) ? + GIT_SUBMODULE_IGNORE_NONE : submodule->ignore; +} + +int git_submodule_set_ignore(git_repository *repo, const char *name, git_submodule_ignore_t ignore) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + return write_mapped_var(repo, name, _sm_ignore_map, ARRAY_SIZE(_sm_ignore_map), "ignore", ignore); +} + +git_submodule_update_t git_submodule_update_strategy(git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, GIT_SUBMODULE_UPDATE_NONE); + + return (submodule->update < GIT_SUBMODULE_UPDATE_CHECKOUT) ? + GIT_SUBMODULE_UPDATE_CHECKOUT : submodule->update; +} + +int git_submodule_set_update(git_repository *repo, const char *name, git_submodule_update_t update) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + return write_mapped_var(repo, name, _sm_update_map, ARRAY_SIZE(_sm_update_map), "update", update); +} + +git_submodule_recurse_t git_submodule_fetch_recurse_submodules( + git_submodule *submodule) +{ + GIT_ASSERT_ARG_WITH_RETVAL(submodule, GIT_SUBMODULE_RECURSE_NO); + return submodule->fetch_recurse; +} + +int git_submodule_set_fetch_recurse_submodules(git_repository *repo, const char *name, git_submodule_recurse_t recurse) +{ + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + return write_mapped_var(repo, name, _sm_recurse_map, ARRAY_SIZE(_sm_recurse_map), "fetchRecurseSubmodules", recurse); +} + +static int submodule_repo_create( + git_repository **out, + git_repository *parent_repo, + const char *path) +{ + int error = 0; + git_str workdir = GIT_STR_INIT, repodir = GIT_STR_INIT; + git_repository_init_options initopt = GIT_REPOSITORY_INIT_OPTIONS_INIT; + git_repository *subrepo = NULL; + + initopt.flags = + GIT_REPOSITORY_INIT_MKPATH | + GIT_REPOSITORY_INIT_NO_REINIT | + GIT_REPOSITORY_INIT_NO_DOTGIT_DIR | + GIT_REPOSITORY_INIT_RELATIVE_GITLINK; + + /* Workdir: path to sub-repo working directory */ + error = git_repository_workdir_path(&workdir, parent_repo, path); + if (error < 0) + goto cleanup; + + initopt.workdir_path = workdir.ptr; + + /** + * Repodir: path to the sub-repo. sub-repo goes in: + * /modules// with a gitlink in the + * sub-repo workdir directory to that repository. + */ + error = git_repository__item_path(&repodir, parent_repo, GIT_REPOSITORY_ITEM_MODULES); + if (error < 0) + goto cleanup; + error = git_str_joinpath(&repodir, repodir.ptr, path); + if (error < 0) + goto cleanup; + + error = git_repository_init_ext(&subrepo, repodir.ptr, &initopt); + +cleanup: + git_str_dispose(&workdir); + git_str_dispose(&repodir); + + *out = subrepo; + + return error; +} + +/** + * Callback to override sub-repository creation when + * cloning a sub-repository. + */ +static int git_submodule_update_repo_init_cb( + git_repository **out, + const char *path, + int bare, + void *payload) +{ + git_submodule *sm; + + GIT_UNUSED(bare); + + sm = payload; + + return submodule_repo_create(out, sm->repo, path); +} + +int git_submodule_update_options_init(git_submodule_update_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_submodule_update_options, GIT_SUBMODULE_UPDATE_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_submodule_update_init_options(git_submodule_update_options *opts, unsigned int version) +{ + return git_submodule_update_options_init(opts, version); +} +#endif + +int git_submodule_update(git_submodule *sm, int init, git_submodule_update_options *_update_options) +{ + int error; + unsigned int submodule_status; + git_config *config = NULL; + const char *submodule_url; + git_repository *sub_repo = NULL; + git_remote *remote = NULL; + git_object *target_commit = NULL; + git_str buf = GIT_STR_INIT; + git_submodule_update_options update_options = GIT_SUBMODULE_UPDATE_OPTIONS_INIT; + git_clone_options clone_options = GIT_CLONE_OPTIONS_INIT; + + GIT_ASSERT_ARG(sm); + + if (_update_options) + memcpy(&update_options, _update_options, sizeof(git_submodule_update_options)); + + GIT_ERROR_CHECK_VERSION(&update_options, GIT_SUBMODULE_UPDATE_OPTIONS_VERSION, "git_submodule_update_options"); + + /* Copy over the remote callbacks */ + memcpy(&clone_options.fetch_opts, &update_options.fetch_opts, sizeof(git_fetch_options)); + + /* Get the status of the submodule to determine if it is already initialized */ + if ((error = git_submodule_status(&submodule_status, sm->repo, sm->name, GIT_SUBMODULE_IGNORE_UNSPECIFIED)) < 0) + goto done; + + /* If the submodule is configured but hasn't been added, skip it */ + if (submodule_status == GIT_SUBMODULE_STATUS_IN_CONFIG) + goto done; + + /* + * If submodule work dir is not already initialized, check to see + * what we need to do (initialize, clone, return error...) + */ + if (submodule_status & GIT_SUBMODULE_STATUS_WD_UNINITIALIZED) { + /* + * Work dir is not initialized, check to see if the submodule + * info has been copied into .git/config + */ + if ((error = git_repository_config_snapshot(&config, sm->repo)) < 0 || + (error = git_str_printf(&buf, "submodule.%s.url", git_submodule_name(sm))) < 0) + goto done; + + if ((error = git_config_get_string(&submodule_url, config, git_str_cstr(&buf))) < 0) { + /* + * If the error is not "not found" or if it is "not found" and we are not + * initializing the submodule, then return error. + */ + if (error != GIT_ENOTFOUND) + goto done; + + if (!init) { + git_error_set(GIT_ERROR_SUBMODULE, "submodule is not initialized"); + error = GIT_ERROR; + goto done; + } + + /* The submodule has not been initialized yet - initialize it now.*/ + if ((error = git_submodule_init(sm, 0)) < 0) + goto done; + + git_config_free(config); + config = NULL; + + if ((error = git_repository_config_snapshot(&config, sm->repo)) < 0 || + (error = git_config_get_string(&submodule_url, config, git_str_cstr(&buf))) < 0) + goto done; + } + + /** submodule is initialized - now clone it **/ + /* override repo creation */ + clone_options.repository_cb = git_submodule_update_repo_init_cb; + clone_options.repository_cb_payload = sm; + + /* + * Do not perform checkout as part of clone, instead we + * will checkout the specific commit manually. + */ + clone_options.checkout_opts.checkout_strategy = GIT_CHECKOUT_NONE; + + if ((error = git_clone__submodule(&sub_repo, submodule_url, sm->path, &clone_options)) < 0 || + (error = git_repository_set_head_detached(sub_repo, git_submodule_index_id(sm))) < 0 || + (error = git_checkout_head(sub_repo, &update_options.checkout_opts)) != 0) + goto done; + } else { + const git_oid *oid; + + /** + * Work dir is initialized - look up the commit in the parent repository's index, + * update the workdir contents of the subrepository, and set the subrepository's + * head to the new commit. + */ + if ((error = git_submodule_open(&sub_repo, sm)) < 0) + goto done; + + if ((oid = git_submodule_index_id(sm)) == NULL) { + git_error_set(GIT_ERROR_SUBMODULE, "could not get ID of submodule in index"); + error = -1; + goto done; + } + + /* Look up the target commit in the submodule. */ + if ((error = git_object_lookup(&target_commit, sub_repo, oid, GIT_OBJECT_COMMIT)) < 0) { + /* If it isn't found then fetch and try again. */ + if (error != GIT_ENOTFOUND || !update_options.allow_fetch || + (error = lookup_default_remote(&remote, sub_repo)) < 0 || + (error = git_remote_fetch(remote, NULL, &update_options.fetch_opts, NULL)) < 0 || + (error = git_object_lookup(&target_commit, sub_repo, git_submodule_index_id(sm), GIT_OBJECT_COMMIT)) < 0) + goto done; + } + + if ((error = git_checkout_tree(sub_repo, target_commit, &update_options.checkout_opts)) != 0 || + (error = git_repository_set_head_detached(sub_repo, git_submodule_index_id(sm))) < 0) + goto done; + + /* Invalidate the wd flags as the workdir has been updated. */ + sm->flags = sm->flags & + ~(GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS__WD_OID_VALID | + GIT_SUBMODULE_STATUS__WD_SCANNED); + } + +done: + git_str_dispose(&buf); + git_config_free(config); + git_object_free(target_commit); + git_remote_free(remote); + git_repository_free(sub_repo); + + return error; +} + +int git_submodule_init(git_submodule *sm, int overwrite) +{ + int error; + const char *val; + git_str key = GIT_STR_INIT, effective_submodule_url = GIT_STR_INIT; + git_config *cfg = NULL; + + if (!sm->url) { + git_error_set(GIT_ERROR_SUBMODULE, + "no URL configured for submodule '%s'", sm->name); + return -1; + } + + if ((error = git_repository_config(&cfg, sm->repo)) < 0) + return error; + + /* write "submodule.NAME.url" */ + + if ((error = git_submodule__resolve_url(&effective_submodule_url, sm->repo, sm->url)) < 0 || + (error = git_str_printf(&key, "submodule.%s.url", sm->name)) < 0 || + (error = git_config__update_entry( + cfg, key.ptr, effective_submodule_url.ptr, overwrite != 0, false)) < 0) + goto cleanup; + + /* write "submodule.NAME.update" if not default */ + + val = (sm->update == GIT_SUBMODULE_UPDATE_CHECKOUT) ? + NULL : submodule_update_to_str(sm->update); + + if ((error = git_str_printf(&key, "submodule.%s.update", sm->name)) < 0 || + (error = git_config__update_entry( + cfg, key.ptr, val, overwrite != 0, false)) < 0) + goto cleanup; + + /* success */ + +cleanup: + git_config_free(cfg); + git_str_dispose(&key); + git_str_dispose(&effective_submodule_url); + + return error; +} + +int git_submodule_sync(git_submodule *sm) +{ + git_str key = GIT_STR_INIT, url = GIT_STR_INIT, remote_name = GIT_STR_INIT; + git_repository *smrepo = NULL; + git_config *cfg = NULL; + int error = 0; + + if (!sm->url) { + git_error_set(GIT_ERROR_SUBMODULE, "no URL configured for submodule '%s'", sm->name); + return -1; + } + + /* copy URL over to config only if it already exists */ + if ((error = git_repository_config__weakptr(&cfg, sm->repo)) < 0 || + (error = git_str_printf(&key, "submodule.%s.url", sm->name)) < 0 || + (error = git_submodule__resolve_url(&url, sm->repo, sm->url)) < 0 || + (error = git_config__update_entry(cfg, key.ptr, url.ptr, true, true)) < 0) + goto out; + + if (!(sm->flags & GIT_SUBMODULE_STATUS_IN_WD)) + goto out; + + /* if submodule exists in the working directory, update remote url */ + if ((error = git_submodule_open(&smrepo, sm)) < 0 || + (error = git_repository_config__weakptr(&cfg, smrepo)) < 0) + goto out; + + if (lookup_head_remote_key(&remote_name, smrepo) == 0) { + if ((error = git_str_join3(&key, '.', "remote", remote_name.ptr, "url")) < 0) + goto out; + } else if ((error = git_str_sets(&key, "remote.origin.url")) < 0) { + goto out; + } + + if ((error = git_config__update_entry(cfg, key.ptr, url.ptr, true, false)) < 0) + goto out; + +out: + git_repository_free(smrepo); + git_str_dispose(&remote_name); + git_str_dispose(&key); + git_str_dispose(&url); + return error; +} + +static int git_submodule__open( + git_repository **subrepo, git_submodule *sm, bool bare) +{ + int error; + git_str path = GIT_STR_INIT; + unsigned int flags = GIT_REPOSITORY_OPEN_NO_SEARCH; + const char *wd; + + GIT_ASSERT_ARG(sm); + GIT_ASSERT_ARG(subrepo); + + if (git_repository__ensure_not_bare( + sm->repo, "open submodule repository") < 0) + return GIT_EBAREREPO; + + wd = git_repository_workdir(sm->repo); + + if (git_str_join3(&path, '/', wd, sm->path, DOT_GIT) < 0) + return -1; + + sm->flags = sm->flags & + ~(GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS__WD_OID_VALID | + GIT_SUBMODULE_STATUS__WD_SCANNED); + + if (bare) + flags |= GIT_REPOSITORY_OPEN_BARE; + + error = git_repository_open_ext(subrepo, path.ptr, flags, wd); + + /* if we opened the submodule successfully, grab HEAD OID, etc. */ + if (!error) { + sm->flags |= GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS__WD_SCANNED; + + if (!git_reference_name_to_id(&sm->wd_oid, *subrepo, GIT_HEAD_FILE)) + sm->flags |= GIT_SUBMODULE_STATUS__WD_OID_VALID; + else + git_error_clear(); + } else if (git_fs_path_exists(path.ptr)) { + sm->flags |= GIT_SUBMODULE_STATUS__WD_SCANNED | + GIT_SUBMODULE_STATUS_IN_WD; + } else { + git_str_rtruncate_at_char(&path, '/'); /* remove "/.git" */ + + if (git_fs_path_isdir(path.ptr)) + sm->flags |= GIT_SUBMODULE_STATUS__WD_SCANNED; + } + + git_str_dispose(&path); + + return error; +} + +int git_submodule_open_bare(git_repository **subrepo, git_submodule *sm) +{ + return git_submodule__open(subrepo, sm, true); +} + +int git_submodule_open(git_repository **subrepo, git_submodule *sm) +{ + return git_submodule__open(subrepo, sm, false); +} + +static void submodule_update_from_index_entry( + git_submodule *sm, const git_index_entry *ie) +{ + bool already_found = (sm->flags & GIT_SUBMODULE_STATUS_IN_INDEX) != 0; + + if (!S_ISGITLINK(ie->mode)) { + if (!already_found) + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE; + } else { + if (already_found) + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_MULTIPLE_ENTRIES; + else + git_oid_cpy(&sm->index_oid, &ie->id); + + sm->flags |= GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS__INDEX_OID_VALID; + } +} + +static int submodule_update_index(git_submodule *sm) +{ + git_index *index; + const git_index_entry *ie; + + if (git_repository_index__weakptr(&index, sm->repo) < 0) + return -1; + + sm->flags = sm->flags & + ~(GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS__INDEX_OID_VALID); + + if (!(ie = git_index_get_bypath(index, sm->path, 0))) + return 0; + + submodule_update_from_index_entry(sm, ie); + + return 0; +} + +static void submodule_update_from_head_data( + git_submodule *sm, mode_t mode, const git_oid *id) +{ + if (!S_ISGITLINK(mode)) + sm->flags |= GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE; + else { + git_oid_cpy(&sm->head_oid, id); + + sm->flags |= GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS__HEAD_OID_VALID; + } +} + +static int submodule_update_head(git_submodule *submodule) +{ + git_tree *head = NULL; + git_tree_entry *te = NULL; + + submodule->flags = submodule->flags & + ~(GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS__HEAD_OID_VALID); + + /* if we can't look up file in current head, then done */ + if (git_repository_head_tree(&head, submodule->repo) < 0 || + git_tree_entry_bypath(&te, head, submodule->path) < 0) + git_error_clear(); + else + submodule_update_from_head_data(submodule, te->attr, git_tree_entry_id(te)); + + git_tree_entry_free(te); + git_tree_free(head); + return 0; +} + +int git_submodule_reload(git_submodule *sm, int force) +{ + git_config *mods = NULL; + int error; + + GIT_UNUSED(force); + + GIT_ASSERT_ARG(sm); + + if ((error = git_submodule_name_is_valid(sm->repo, sm->name, 0)) <= 0) + /* This should come with a warning, but we've no API for that */ + goto out; + + if (git_repository_is_bare(sm->repo)) + goto out; + + /* refresh config data */ + if ((error = gitmodules_snapshot(&mods, sm->repo)) < 0 && error != GIT_ENOTFOUND) + goto out; + + if (mods != NULL && (error = submodule_read_config(sm, mods)) < 0) + goto out; + + /* refresh wd data */ + sm->flags &= + ~(GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS__WD_OID_VALID | + GIT_SUBMODULE_STATUS__WD_FLAGS); + + if ((error = submodule_load_from_wd_lite(sm)) < 0 || + (error = submodule_update_index(sm)) < 0 || + (error = submodule_update_head(sm)) < 0) + goto out; + +out: + git_config_free(mods); + return error; +} + +static void submodule_copy_oid_maybe( + git_oid *tgt, const git_oid *src, bool valid) +{ + if (tgt) { + if (valid) + memcpy(tgt, src, sizeof(*tgt)); + else + memset(tgt, 0, sizeof(*tgt)); + } +} + +int git_submodule__status( + unsigned int *out_status, + git_oid *out_head_id, + git_oid *out_index_id, + git_oid *out_wd_id, + git_submodule *sm, + git_submodule_ignore_t ign) +{ + unsigned int status; + git_repository *smrepo = NULL; + + if (ign == GIT_SUBMODULE_IGNORE_UNSPECIFIED) + ign = sm->ignore; + + /* only return location info if ignore == all */ + if (ign == GIT_SUBMODULE_IGNORE_ALL) { + *out_status = (sm->flags & GIT_SUBMODULE_STATUS__IN_FLAGS); + return 0; + } + + /* If the user has requested caching submodule state, performing these + * expensive operations (especially `submodule_update_head`, which is + * bottlenecked on `git_repository_head_tree`) eliminates much of the + * advantage. We will, therefore, interpret the request for caching to + * apply here to and skip them. + */ + + if (sm->repo->submodule_cache == NULL) { + /* refresh the index OID */ + if (submodule_update_index(sm) < 0) + return -1; + + /* refresh the HEAD OID */ + if (submodule_update_head(sm) < 0) + return -1; + } + + /* for ignore == dirty, don't scan the working directory */ + if (ign == GIT_SUBMODULE_IGNORE_DIRTY) { + /* git_submodule_open_bare will load WD OID data */ + if (git_submodule_open_bare(&smrepo, sm) < 0) + git_error_clear(); + else + git_repository_free(smrepo); + smrepo = NULL; + } else if (git_submodule_open(&smrepo, sm) < 0) { + git_error_clear(); + smrepo = NULL; + } + + status = GIT_SUBMODULE_STATUS__CLEAR_INTERNAL(sm->flags); + + submodule_get_index_status(&status, sm); + submodule_get_wd_status(&status, sm, smrepo, ign); + + git_repository_free(smrepo); + + *out_status = status; + + submodule_copy_oid_maybe(out_head_id, &sm->head_oid, + (sm->flags & GIT_SUBMODULE_STATUS__HEAD_OID_VALID) != 0); + submodule_copy_oid_maybe(out_index_id, &sm->index_oid, + (sm->flags & GIT_SUBMODULE_STATUS__INDEX_OID_VALID) != 0); + submodule_copy_oid_maybe(out_wd_id, &sm->wd_oid, + (sm->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) != 0); + + return 0; +} + +int git_submodule_status(unsigned int *status, git_repository *repo, const char *name, git_submodule_ignore_t ignore) +{ + git_submodule *sm; + int error; + + GIT_ASSERT_ARG(status); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + if ((error = git_submodule_lookup(&sm, repo, name)) < 0) + return error; + + error = git_submodule__status(status, NULL, NULL, NULL, sm, ignore); + git_submodule_free(sm); + + return error; +} + +int git_submodule_location(unsigned int *location, git_submodule *sm) +{ + GIT_ASSERT_ARG(location); + GIT_ASSERT_ARG(sm); + + return git_submodule__status( + location, NULL, NULL, NULL, sm, GIT_SUBMODULE_IGNORE_ALL); +} + +/* + * INTERNAL FUNCTIONS + */ + +static int submodule_alloc( + git_submodule **out, git_repository *repo, const char *name) +{ + size_t namelen; + git_submodule *sm; + + if (!name || !(namelen = strlen(name))) { + git_error_set(GIT_ERROR_SUBMODULE, "invalid submodule name"); + return -1; + } + + sm = git__calloc(1, sizeof(git_submodule)); + GIT_ERROR_CHECK_ALLOC(sm); + + sm->name = sm->path = git__strdup(name); + if (!sm->name) { + git__free(sm); + return -1; + } + + GIT_REFCOUNT_INC(sm); + sm->ignore = sm->ignore_default = GIT_SUBMODULE_IGNORE_NONE; + sm->update = sm->update_default = GIT_SUBMODULE_UPDATE_CHECKOUT; + sm->fetch_recurse = sm->fetch_recurse_default = GIT_SUBMODULE_RECURSE_NO; + sm->repo = repo; + sm->branch = NULL; + + *out = sm; + return 0; +} + +static void submodule_release(git_submodule *sm) +{ + if (!sm) + return; + + if (sm->repo) { + sm->repo = NULL; + } + + if (sm->path != sm->name) + git__free(sm->path); + git__free(sm->name); + git__free(sm->url); + git__free(sm->branch); + git__memzero(sm, sizeof(*sm)); + git__free(sm); +} + +int git_submodule_dup(git_submodule **out, git_submodule *source) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(source); + + GIT_REFCOUNT_INC(source); + + *out = source; + return 0; +} + +void git_submodule_free(git_submodule *sm) +{ + if (!sm) + return; + GIT_REFCOUNT_DEC(sm, submodule_release); +} + +static int submodule_config_error(const char *property, const char *value) +{ + git_error_set(GIT_ERROR_INVALID, + "invalid value for submodule '%s' property: '%s'", property, value); + return -1; +} + +int git_submodule_parse_ignore(git_submodule_ignore_t *out, const char *value) +{ + int val; + + if (git_config_lookup_map_value( + &val, _sm_ignore_map, ARRAY_SIZE(_sm_ignore_map), value) < 0) { + *out = GIT_SUBMODULE_IGNORE_NONE; + return submodule_config_error("ignore", value); + } + + *out = (git_submodule_ignore_t)val; + return 0; +} + +int git_submodule_parse_update(git_submodule_update_t *out, const char *value) +{ + int val; + + if (git_config_lookup_map_value( + &val, _sm_update_map, ARRAY_SIZE(_sm_update_map), value) < 0) { + *out = GIT_SUBMODULE_UPDATE_CHECKOUT; + return submodule_config_error("update", value); + } + + *out = (git_submodule_update_t)val; + return 0; +} + +static int submodule_parse_recurse(git_submodule_recurse_t *out, const char *value) +{ + int val; + + if (git_config_lookup_map_value( + &val, _sm_recurse_map, ARRAY_SIZE(_sm_recurse_map), value) < 0) { + *out = GIT_SUBMODULE_RECURSE_YES; + return submodule_config_error("recurse", value); + } + + *out = (git_submodule_recurse_t)val; + return 0; +} + +static int get_value(const char **out, git_config *cfg, git_str *buf, const char *name, const char *field) +{ + int error; + + git_str_clear(buf); + + if ((error = git_str_printf(buf, "submodule.%s.%s", name, field)) < 0 || + (error = git_config_get_string(out, cfg, buf->ptr)) < 0) + return error; + + return error; +} + +static bool looks_like_command_line_option(const char *s) +{ + if (s && s[0] == '-') + return true; + + return false; +} + +static int submodule_read_config(git_submodule *sm, git_config *cfg) +{ + git_str key = GIT_STR_INIT; + const char *value; + int error, in_config = 0; + + /* + * TODO: Look up path in index and if it is present but not a GITLINK + * then this should be deleted (at least to match git's behavior) + */ + + if ((error = get_value(&value, cfg, &key, sm->name, "path")) == 0) { + in_config = 1; + /* We would warn here if we had that API */ + if (!looks_like_command_line_option(value)) { + /* + * TODO: if case insensitive filesystem, then the following strcmp + * should be strcasecmp + */ + if (strcmp(sm->name, value) != 0) { + if (sm->path != sm->name) + git__free(sm->path); + sm->path = git__strdup(value); + GIT_ERROR_CHECK_ALLOC(sm->path); + } + + } + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if ((error = get_value(&value, cfg, &key, sm->name, "url")) == 0) { + /* We would warn here if we had that API */ + if (!looks_like_command_line_option(value)) { + in_config = 1; + sm->url = git__strdup(value); + GIT_ERROR_CHECK_ALLOC(sm->url); + } + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if ((error = get_value(&value, cfg, &key, sm->name, "branch")) == 0) { + in_config = 1; + sm->branch = git__strdup(value); + GIT_ERROR_CHECK_ALLOC(sm->branch); + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if ((error = get_value(&value, cfg, &key, sm->name, "update")) == 0) { + in_config = 1; + if ((error = git_submodule_parse_update(&sm->update, value)) < 0) + goto cleanup; + sm->update_default = sm->update; + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if ((error = get_value(&value, cfg, &key, sm->name, "fetchRecurseSubmodules")) == 0) { + in_config = 1; + if ((error = submodule_parse_recurse(&sm->fetch_recurse, value)) < 0) + goto cleanup; + sm->fetch_recurse_default = sm->fetch_recurse; + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if ((error = get_value(&value, cfg, &key, sm->name, "ignore")) == 0) { + in_config = 1; + if ((error = git_submodule_parse_ignore(&sm->ignore, value)) < 0) + goto cleanup; + sm->ignore_default = sm->ignore; + } else if (error != GIT_ENOTFOUND) { + goto cleanup; + } + + if (in_config) + sm->flags |= GIT_SUBMODULE_STATUS_IN_CONFIG; + + error = 0; + +cleanup: + git_str_dispose(&key); + return error; +} + +static int submodule_load_each(const git_config_entry *entry, void *payload) +{ + lfc_data *data = payload; + const char *namestart, *property; + git_strmap *map = data->map; + git_str name = GIT_STR_INIT; + git_submodule *sm; + int error, isvalid; + + if (git__prefixcmp(entry->name, "submodule.") != 0) + return 0; + + namestart = entry->name + strlen("submodule."); + property = strrchr(namestart, '.'); + + if (!property || (property == namestart)) + return 0; + + property++; + + if ((error = git_str_set(&name, namestart, property - namestart -1)) < 0) + return error; + + isvalid = git_submodule_name_is_valid(data->repo, name.ptr, 0); + if (isvalid <= 0) { + error = isvalid; + goto done; + } + + /* + * Now that we have the submodule's name, we can use that to + * figure out whether it's in the map. If it's not, we create + * a new submodule, load the config and insert it. If it's + * already inserted, we've already loaded it, so we skip. + */ + if (git_strmap_exists(map, name.ptr)) { + error = 0; + goto done; + } + + if ((error = submodule_alloc(&sm, data->repo, name.ptr)) < 0) + goto done; + + if ((error = submodule_read_config(sm, data->mods)) < 0) { + git_submodule_free(sm); + goto done; + } + + if ((error = git_strmap_set(map, sm->name, sm)) < 0) + goto done; + + error = 0; + +done: + git_str_dispose(&name); + return error; +} + +static int submodule_load_from_wd_lite(git_submodule *sm) +{ + git_str path = GIT_STR_INIT; + + if (git_repository_workdir_path(&path, sm->repo, sm->path) < 0) + return -1; + + if (git_fs_path_isdir(path.ptr)) + sm->flags |= GIT_SUBMODULE_STATUS__WD_SCANNED; + + if (git_fs_path_contains(&path, DOT_GIT)) + sm->flags |= GIT_SUBMODULE_STATUS_IN_WD; + + git_str_dispose(&path); + return 0; +} + +/** + * Requests a snapshot of $WORK_TREE/.gitmodules. + * + * Returns GIT_ENOTFOUND in case no .gitmodules file exist + */ +static int gitmodules_snapshot(git_config **snap, git_repository *repo) +{ + git_config *mods = NULL; + git_str path = GIT_STR_INIT; + int error; + + if (git_repository_workdir(repo) == NULL) + return GIT_ENOTFOUND; + + if ((error = git_repository_workdir_path(&path, repo, GIT_MODULES_FILE)) < 0) + return error; + + if ((error = git_config_open_ondisk(&mods, path.ptr)) < 0) + goto cleanup; + git_str_dispose(&path); + + if ((error = git_config_snapshot(snap, mods)) < 0) + goto cleanup; + + error = 0; + +cleanup: + if (mods) + git_config_free(mods); + git_str_dispose(&path); + + return error; +} + +static git_config_backend *open_gitmodules( + git_repository *repo, + int okay_to_create) +{ + git_str path = GIT_STR_INIT; + git_config_backend *mods = NULL; + + if (git_repository_workdir(repo) != NULL) { + if (git_repository_workdir_path(&path, repo, GIT_MODULES_FILE) != 0) + return NULL; + + if (okay_to_create || git_fs_path_isfile(path.ptr)) { + /* git_config_backend_from_file should only fail if OOM */ + if (git_config_backend_from_file(&mods, path.ptr) < 0) + mods = NULL; + /* open should only fail here if the file is malformed */ + else if (git_config_backend_open(mods, GIT_CONFIG_LEVEL_LOCAL, repo) < 0) { + git_config_backend_free(mods); + mods = NULL; + } + } + } + + git_str_dispose(&path); + + return mods; +} + +/* Lookup name of remote of the local tracking branch HEAD points to */ +static int lookup_head_remote_key(git_str *remote_name, git_repository *repo) +{ + int error; + git_reference *head = NULL; + git_str upstream_name = GIT_STR_INIT; + + /* lookup and dereference HEAD */ + if ((error = git_repository_head(&head, repo)) < 0) + return error; + + /** + * If head does not refer to a branch, then return + * GIT_ENOTFOUND to indicate that we could not find + * a remote key for the local tracking branch HEAD points to. + **/ + if (!git_reference_is_branch(head)) { + git_error_set(GIT_ERROR_INVALID, + "HEAD does not refer to a branch."); + error = GIT_ENOTFOUND; + goto done; + } + + /* lookup remote tracking branch of HEAD */ + if ((error = git_branch__upstream_name( + &upstream_name, + repo, + git_reference_name(head))) < 0) + goto done; + + /* lookup remote of remote tracking branch */ + if ((error = git_branch__remote_name(remote_name, repo, upstream_name.ptr)) < 0) + goto done; + +done: + git_str_dispose(&upstream_name); + git_reference_free(head); + + return error; +} + +/* Lookup the remote of the local tracking branch HEAD points to */ +static int lookup_head_remote(git_remote **remote, git_repository *repo) +{ + int error; + git_str remote_name = GIT_STR_INIT; + + /* lookup remote of remote tracking branch name */ + if (!(error = lookup_head_remote_key(&remote_name, repo))) + error = git_remote_lookup(remote, repo, remote_name.ptr); + + git_str_dispose(&remote_name); + + return error; +} + +/* Lookup remote, either from HEAD or fall back on origin */ +static int lookup_default_remote(git_remote **remote, git_repository *repo) +{ + int error = lookup_head_remote(remote, repo); + + /* if that failed, use 'origin' instead */ + if (error == GIT_ENOTFOUND || error == GIT_EUNBORNBRANCH) + error = git_remote_lookup(remote, repo, "origin"); + + if (error == GIT_ENOTFOUND) + git_error_set( + GIT_ERROR_SUBMODULE, + "cannot get default remote for submodule - no local tracking " + "branch for HEAD and origin does not exist"); + + return error; +} + +static int get_url_base(git_str *url, git_repository *repo) +{ + int error; + git_worktree *wt = NULL; + git_remote *remote = NULL; + + if ((error = lookup_default_remote(&remote, repo)) == 0) { + error = git_str_sets(url, git_remote_url(remote)); + goto out; + } else if (error != GIT_ENOTFOUND) + goto out; + else + git_error_clear(); + + /* if repository does not have a default remote, use workdir instead */ + if (git_repository_is_worktree(repo)) { + if ((error = git_worktree_open_from_repository(&wt, repo)) < 0) + goto out; + error = git_str_sets(url, wt->parent_path); + } else { + error = git_str_sets(url, git_repository_workdir(repo)); + } + +out: + git_remote_free(remote); + git_worktree_free(wt); + + return error; +} + +static void submodule_get_index_status(unsigned int *status, git_submodule *sm) +{ + const git_oid *head_oid = git_submodule_head_id(sm); + const git_oid *index_oid = git_submodule_index_id(sm); + + *status = *status & ~GIT_SUBMODULE_STATUS__INDEX_FLAGS; + + if (!head_oid) { + if (index_oid) + *status |= GIT_SUBMODULE_STATUS_INDEX_ADDED; + } + else if (!index_oid) + *status |= GIT_SUBMODULE_STATUS_INDEX_DELETED; + else if (!git_oid_equal(head_oid, index_oid)) + *status |= GIT_SUBMODULE_STATUS_INDEX_MODIFIED; +} + + +static void submodule_get_wd_status( + unsigned int *status, + git_submodule *sm, + git_repository *sm_repo, + git_submodule_ignore_t ign) +{ + const git_oid *index_oid = git_submodule_index_id(sm); + const git_oid *wd_oid = + (sm->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) ? &sm->wd_oid : NULL; + git_tree *sm_head = NULL; + git_index *index = NULL; + git_diff_options opt = GIT_DIFF_OPTIONS_INIT; + git_diff *diff; + + *status = *status & ~GIT_SUBMODULE_STATUS__WD_FLAGS; + + if (!index_oid) { + if (wd_oid) + *status |= GIT_SUBMODULE_STATUS_WD_ADDED; + } + else if (!wd_oid) { + if ((sm->flags & GIT_SUBMODULE_STATUS__WD_SCANNED) != 0 && + (sm->flags & GIT_SUBMODULE_STATUS_IN_WD) == 0) + *status |= GIT_SUBMODULE_STATUS_WD_UNINITIALIZED; + else + *status |= GIT_SUBMODULE_STATUS_WD_DELETED; + } + else if (!git_oid_equal(index_oid, wd_oid)) + *status |= GIT_SUBMODULE_STATUS_WD_MODIFIED; + + /* if we have no repo, then we're done */ + if (!sm_repo) + return; + + /* the diffs below could be optimized with an early termination + * option to the git_diff functions, but for now this is sufficient + * (and certainly no worse that what core git does). + */ + + if (ign == GIT_SUBMODULE_IGNORE_NONE) + opt.flags |= GIT_DIFF_INCLUDE_UNTRACKED; + + (void)git_repository_index__weakptr(&index, sm_repo); + + /* if we don't have an unborn head, check diff with index */ + if (git_repository_head_tree(&sm_head, sm_repo) < 0) + git_error_clear(); + else { + /* perform head to index diff on submodule */ + if (git_diff_tree_to_index(&diff, sm_repo, sm_head, index, &opt) < 0) + git_error_clear(); + else { + if (git_diff_num_deltas(diff) > 0) + *status |= GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED; + git_diff_free(diff); + diff = NULL; + } + + git_tree_free(sm_head); + } + + /* perform index-to-workdir diff on submodule */ + if (git_diff_index_to_workdir(&diff, sm_repo, index, &opt) < 0) + git_error_clear(); + else { + size_t untracked = + git_diff_num_deltas_of_type(diff, GIT_DELTA_UNTRACKED); + + if (untracked > 0) + *status |= GIT_SUBMODULE_STATUS_WD_UNTRACKED; + + if (git_diff_num_deltas(diff) != untracked) + *status |= GIT_SUBMODULE_STATUS_WD_WD_MODIFIED; + + git_diff_free(diff); + diff = NULL; + } +} diff --git a/src/libgit2/submodule.h b/src/libgit2/submodule.h new file mode 100644 index 0000000..40b7b70 --- /dev/null +++ b/src/libgit2/submodule.h @@ -0,0 +1,164 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_submodule_h__ +#define INCLUDE_submodule_h__ + +#include "common.h" + +#include "git2/submodule.h" +#include "git2/repository.h" +#include "futils.h" + +/* Notes: + * + * Submodule information can be in four places: the index, the config files + * (both .git/config and .gitmodules), the HEAD tree, and the working + * directory. + * + * In the index: + * - submodule is found by path + * - may be missing, present, or of the wrong type + * - will have an oid if present + * + * In the HEAD tree: + * - submodule is found by path + * - may be missing, present, or of the wrong type + * - will have an oid if present + * + * In the config files: + * - submodule is found by submodule "name" which is usually the path + * - may be missing or present + * - will have a name, path, url, and other properties + * + * In the working directory: + * - submodule is found by path + * - may be missing, an empty directory, a checked out directory, + * or of the wrong type + * - if checked out, will have a HEAD oid + * - if checked out, will have git history that can be used to compare oids + * - if checked out, may have modified files and/or untracked files + */ + +/** + * Description of submodule + * + * This record describes a submodule found in a repository. There should be + * an entry for every submodule found in the HEAD and index, and for every + * submodule described in .gitmodules. The fields are as follows: + * + * - `rc` tracks the refcount of how many hash table entries in the + * git_submodule_cache there are for this submodule. It only comes into + * play if the name and path of the submodule differ. + * + * - `name` is the name of the submodule from .gitmodules. + * - `path` is the path to the submodule from the repo root. It is almost + * always the same as `name`. + * - `url` is the url for the submodule. + * - `update` is a git_submodule_update_t value - see gitmodules(5) update. + * - `update_default` is the update value from the config + * - `ignore` is a git_submodule_ignore_t value - see gitmodules(5) ignore. + * - `ignore_default` is the ignore value from the config + * - `fetch_recurse` is a git_submodule_recurse_t value - see gitmodules(5) + * fetchRecurseSubmodules. + * - `fetch_recurse_default` is the recurse value from the config + * + * - `repo` is the parent repository that contains this submodule. + * - `flags` after for internal use, tracking where this submodule has been + * found (head, index, config, workdir) and known status info, etc. + * - `head_oid` is the oid for the submodule path in the repo HEAD. + * - `index_oid` is the oid for the submodule recorded in the index. + * - `wd_oid` is the oid for the HEAD of the checked out submodule. + * + * If the submodule has been added to .gitmodules but not yet git added, + * then the `index_oid` will be zero but still marked valid. If the + * submodule has been deleted, but the delete has not been committed yet, + * then the `index_oid` will be set, but the `url` will be NULL. + */ +struct git_submodule { + git_refcount rc; + + /* information from config */ + char *name; + char *path; /* important: may just point to "name" string */ + char *url; + char *branch; + git_submodule_update_t update; + git_submodule_update_t update_default; + git_submodule_ignore_t ignore; + git_submodule_ignore_t ignore_default; + git_submodule_recurse_t fetch_recurse; + git_submodule_recurse_t fetch_recurse_default; + + /* internal information */ + git_repository *repo; + uint32_t flags; + git_oid head_oid; + git_oid index_oid; + git_oid wd_oid; +}; + +/* Additional flags on top of public GIT_SUBMODULE_STATUS values */ +enum { + GIT_SUBMODULE_STATUS__WD_SCANNED = (1u << 20), + GIT_SUBMODULE_STATUS__HEAD_OID_VALID = (1u << 21), + GIT_SUBMODULE_STATUS__INDEX_OID_VALID = (1u << 22), + GIT_SUBMODULE_STATUS__WD_OID_VALID = (1u << 23), + GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE = (1u << 24), + GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE = (1u << 25), + GIT_SUBMODULE_STATUS__WD_NOT_SUBMODULE = (1u << 26), + GIT_SUBMODULE_STATUS__INDEX_MULTIPLE_ENTRIES = (1u << 27) +}; + +#define GIT_SUBMODULE_STATUS__CLEAR_INTERNAL(S) \ + ((S) & ~(0xFFFFFFFFu << 20)) + +/* Initialize an external submodule cache for the provided repo. */ +extern int git_submodule_cache_init(git_strmap **out, git_repository *repo); + +/* Release the resources of the submodule cache. */ +extern int git_submodule_cache_free(git_strmap *cache); + +/* Submodule lookup with an explicit cache */ +extern int git_submodule__lookup_with_cache( + git_submodule **out, git_repository *repo, const char *path, git_strmap *cache); + +/* Internal status fn returns status and optionally the various OIDs */ +extern int git_submodule__status( + unsigned int *out_status, + git_oid *out_head_id, + git_oid *out_index_id, + git_oid *out_wd_id, + git_submodule *sm, + git_submodule_ignore_t ign); + +/* Open submodule repository as bare repo for quick HEAD check, etc. */ +extern int git_submodule_open_bare( + git_repository **repo, + git_submodule *submodule); + +extern int git_submodule_parse_ignore( + git_submodule_ignore_t *out, const char *value); +extern int git_submodule_parse_update( + git_submodule_update_t *out, const char *value); + +extern int git_submodule__map( + git_repository *repo, + git_strmap *map); + +/** + * Check whether a submodule's name is valid. + * + * Check the path against the path validity rules, either the filesystem + * defaults (like checkout does) or whichever you want to compare against. + * + * @param repo the repository which contains the submodule + * @param name the name to check + * @param flags the `GIT_PATH` flags to use for the check (0 to use filesystem defaults) + */ +extern int git_submodule_name_is_valid(git_repository *repo, const char *name, int flags); + +#endif diff --git a/src/libgit2/sysdir.c b/src/libgit2/sysdir.c new file mode 100644 index 0000000..7838a67 --- /dev/null +++ b/src/libgit2/sysdir.c @@ -0,0 +1,650 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "sysdir.h" + +#include "runtime.h" +#include "str.h" +#include "fs_path.h" +#include +#if GIT_WIN32 +# include "fs_path.h" +# include "win32/path_w32.h" +# include "win32/utf-conv.h" +#else +# include +# include +#endif + +#ifdef GIT_WIN32 +# define REG_GITFORWINDOWS_KEY L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Git_is1" +# define REG_GITFORWINDOWS_KEY_WOW64 L"SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Git_is1" + +static int expand_win32_path(git_win32_path dest, const wchar_t *src) +{ + DWORD len = ExpandEnvironmentStringsW(src, dest, GIT_WIN_PATH_UTF16); + + if (!len || len > GIT_WIN_PATH_UTF16) + return -1; + + return 0; +} + +static int win32_path_to_utf8(git_str *dest, const wchar_t *src) +{ + git_win32_utf8_path utf8_path; + + if (git_win32_path_to_utf8(utf8_path, src) < 0) { + git_error_set(GIT_ERROR_OS, "unable to convert path to UTF-8"); + return -1; + } + + /* Convert backslashes to forward slashes */ + git_fs_path_mkposix(utf8_path); + + return git_str_sets(dest, utf8_path); +} + +static git_win32_path mock_registry; +static bool mock_registry_set; + +extern int git_win32__set_registry_system_dir(const wchar_t *mock_sysdir) +{ + if (!mock_sysdir) { + mock_registry[0] = L'\0'; + mock_registry_set = false; + } else { + size_t len = wcslen(mock_sysdir); + + if (len > GIT_WIN_PATH_MAX) { + git_error_set(GIT_ERROR_INVALID, "mock path too long"); + return -1; + } + + wcscpy(mock_registry, mock_sysdir); + mock_registry_set = true; + } + + return 0; +} + +static int lookup_registry_key( + git_win32_path out, + const HKEY hive, + const wchar_t* key, + const wchar_t *value) +{ + HKEY hkey; + DWORD type, size; + int error = GIT_ENOTFOUND; + + /* + * Registry data may not be NUL terminated, provide room to do + * it ourselves. + */ + size = (DWORD)((sizeof(git_win32_path) - 1) * sizeof(wchar_t)); + + if (RegOpenKeyExW(hive, key, 0, KEY_READ, &hkey) != 0) + return GIT_ENOTFOUND; + + if (RegQueryValueExW(hkey, value, NULL, &type, (LPBYTE)out, &size) == 0 && + type == REG_SZ && + size > 0 && + size < sizeof(git_win32_path)) { + size_t wsize = size / sizeof(wchar_t); + size_t len = wsize - 1; + + if (out[wsize - 1] != L'\0') { + len = wsize; + out[wsize] = L'\0'; + } + + if (out[len - 1] == L'\\') + out[len - 1] = L'\0'; + + if (_waccess(out, F_OK) == 0) + error = 0; + } + + RegCloseKey(hkey); + return error; +} + +static int find_sysdir_in_registry(git_win32_path out) +{ + if (mock_registry_set) { + if (mock_registry[0] == L'\0') + return GIT_ENOTFOUND; + + wcscpy(out, mock_registry); + return 0; + } + + if (lookup_registry_key(out, HKEY_CURRENT_USER, REG_GITFORWINDOWS_KEY, L"InstallLocation") == 0 || + lookup_registry_key(out, HKEY_CURRENT_USER, REG_GITFORWINDOWS_KEY_WOW64, L"InstallLocation") == 0 || + lookup_registry_key(out, HKEY_LOCAL_MACHINE, REG_GITFORWINDOWS_KEY, L"InstallLocation") == 0 || + lookup_registry_key(out, HKEY_LOCAL_MACHINE, REG_GITFORWINDOWS_KEY_WOW64, L"InstallLocation") == 0) + return 0; + + return GIT_ENOTFOUND; +} + +static int find_sysdir_in_path(git_win32_path out) +{ + size_t out_len; + + if (git_win32_path_find_executable(out, L"git.exe") < 0 && + git_win32_path_find_executable(out, L"git.cmd") < 0) + return GIT_ENOTFOUND; + + out_len = wcslen(out); + + /* Trim the file name */ + if (out_len <= CONST_STRLEN(L"git.exe")) + return GIT_ENOTFOUND; + + out_len -= CONST_STRLEN(L"git.exe"); + + if (out_len && out[out_len - 1] == L'\\') + out_len--; + + /* + * Git for Windows usually places the command in a 'bin' or + * 'cmd' directory, trim that. + */ + if (out_len >= CONST_STRLEN(L"\\bin") && + wcsncmp(&out[out_len - CONST_STRLEN(L"\\bin")], L"\\bin", CONST_STRLEN(L"\\bin")) == 0) + out_len -= CONST_STRLEN(L"\\bin"); + else if (out_len >= CONST_STRLEN(L"\\cmd") && + wcsncmp(&out[out_len - CONST_STRLEN(L"\\cmd")], L"\\cmd", CONST_STRLEN(L"\\cmd")) == 0) + out_len -= CONST_STRLEN(L"\\cmd"); + + if (!out_len) + return GIT_ENOTFOUND; + + out[out_len] = L'\0'; + return 0; +} + +static int find_win32_dirs( + git_str *out, + const wchar_t* tmpl[]) +{ + git_win32_path path16; + git_str buf = GIT_STR_INIT; + + git_str_clear(out); + + for (; *tmpl != NULL; tmpl++) { + if (!expand_win32_path(path16, *tmpl) && + path16[0] != L'%' && + !_waccess(path16, F_OK)) { + win32_path_to_utf8(&buf, path16); + + if (buf.size) + git_str_join(out, GIT_PATH_LIST_SEPARATOR, out->ptr, buf.ptr); + } + } + + git_str_dispose(&buf); + + return (git_str_oom(out) ? -1 : 0); +} + +static int append_subdir(git_str *out, git_str *path, const char *subdir) +{ + static const char* architecture_roots[] = { + "", + "mingw64", + "mingw32", + NULL + }; + const char **root; + size_t orig_path_len = path->size; + + for (root = architecture_roots; *root; root++) { + if ((*root[0] && git_str_joinpath(path, path->ptr, *root) < 0) || + git_str_joinpath(path, path->ptr, subdir) < 0) + return -1; + + if (git_fs_path_exists(path->ptr) && + git_str_join(out, GIT_PATH_LIST_SEPARATOR, out->ptr, path->ptr) < 0) + return -1; + + git_str_truncate(path, orig_path_len); + } + + return 0; +} + +int git_win32__find_system_dirs(git_str *out, const char *subdir) +{ + git_win32_path pathdir, regdir; + git_str path8 = GIT_STR_INIT; + bool has_pathdir, has_regdir; + int error; + + has_pathdir = (find_sysdir_in_path(pathdir) == 0); + has_regdir = (find_sysdir_in_registry(regdir) == 0); + + if (!has_pathdir && !has_regdir) + return 0; + + /* + * Usually the git in the path is the same git in the registry, + * in this case there's no need to duplicate the paths. + */ + if (has_pathdir && has_regdir && wcscmp(pathdir, regdir) == 0) + has_regdir = false; + + if (has_pathdir) { + if ((error = win32_path_to_utf8(&path8, pathdir)) < 0 || + (error = append_subdir(out, &path8, subdir)) < 0) + goto done; + } + + if (has_regdir) { + if ((error = win32_path_to_utf8(&path8, regdir)) < 0 || + (error = append_subdir(out, &path8, subdir)) < 0) + goto done; + } + +done: + git_str_dispose(&path8); + return error; +} +#endif /* WIN32 */ + +static int git_sysdir_guess_programdata_dirs(git_str *out) +{ +#ifdef GIT_WIN32 + static const wchar_t *programdata_tmpls[2] = { + L"%PROGRAMDATA%\\Git", + NULL, + }; + + return find_win32_dirs(out, programdata_tmpls); +#else + git_str_clear(out); + return 0; +#endif +} + +static int git_sysdir_guess_system_dirs(git_str *out) +{ +#ifdef GIT_WIN32 + return git_win32__find_system_dirs(out, "etc"); +#else + return git_str_sets(out, "/etc"); +#endif +} + +#ifndef GIT_WIN32 +static int get_passwd_home(git_str *out, uid_t uid) +{ + struct passwd pwd, *pwdptr; + char *buf = NULL; + long buflen; + int error; + + GIT_ASSERT_ARG(out); + + if ((buflen = sysconf(_SC_GETPW_R_SIZE_MAX)) == -1) + buflen = 1024; + + do { + buf = git__realloc(buf, buflen); + error = getpwuid_r(uid, &pwd, buf, buflen, &pwdptr); + buflen *= 2; + } while (error == ERANGE && buflen <= 8192); + + if (error) { + git_error_set(GIT_ERROR_OS, "failed to get passwd entry"); + goto out; + } + + if (!pwdptr) { + git_error_set(GIT_ERROR_OS, "no passwd entry found for user"); + goto out; + } + + if ((error = git_str_puts(out, pwdptr->pw_dir)) < 0) + goto out; + +out: + git__free(buf); + return error; +} +#endif + +static int git_sysdir_guess_home_dirs(git_str *out) +{ +#ifdef GIT_WIN32 + static const wchar_t *global_tmpls[4] = { + L"%HOME%\\", + L"%HOMEDRIVE%%HOMEPATH%\\", + L"%USERPROFILE%\\", + NULL, + }; + + return find_win32_dirs(out, global_tmpls); +#else + int error; + uid_t uid, euid; + const char *sandbox_id; + + uid = getuid(); + euid = geteuid(); + + /** + * If APP_SANDBOX_CONTAINER_ID is set, we are running in a + * sandboxed environment on macOS. + */ + sandbox_id = getenv("APP_SANDBOX_CONTAINER_ID"); + + /* + * In case we are running setuid, use the configuration + * of the effective user. + * + * If we are running in a sandboxed environment on macOS, + * we have to get the HOME dir from the password entry file. + */ + if (!sandbox_id && uid == euid) + error = git__getenv(out, "HOME"); + else + error = get_passwd_home(out, euid); + + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = 0; + } + + return error; +#endif +} + +static int git_sysdir_guess_global_dirs(git_str *out) +{ + return git_sysdir_guess_home_dirs(out); +} + +static int git_sysdir_guess_xdg_dirs(git_str *out) +{ +#ifdef GIT_WIN32 + static const wchar_t *global_tmpls[7] = { + L"%XDG_CONFIG_HOME%\\git", + L"%APPDATA%\\git", + L"%LOCALAPPDATA%\\git", + L"%HOME%\\.config\\git", + L"%HOMEDRIVE%%HOMEPATH%\\.config\\git", + L"%USERPROFILE%\\.config\\git", + NULL, + }; + + return find_win32_dirs(out, global_tmpls); +#else + git_str env = GIT_STR_INIT; + int error; + uid_t uid, euid; + + uid = getuid(); + euid = geteuid(); + + /* + * In case we are running setuid, only look up passwd + * directory of the effective user. + */ + if (uid == euid) { + if ((error = git__getenv(&env, "XDG_CONFIG_HOME")) == 0) + error = git_str_joinpath(out, env.ptr, "git"); + + if (error == GIT_ENOTFOUND && (error = git__getenv(&env, "HOME")) == 0) + error = git_str_joinpath(out, env.ptr, ".config/git"); + } else { + if ((error = get_passwd_home(&env, euid)) == 0) + error = git_str_joinpath(out, env.ptr, ".config/git"); + } + + if (error == GIT_ENOTFOUND) { + git_error_clear(); + error = 0; + } + + git_str_dispose(&env); + return error; +#endif +} + +static int git_sysdir_guess_template_dirs(git_str *out) +{ +#ifdef GIT_WIN32 + return git_win32__find_system_dirs(out, "share/git-core/templates"); +#else + return git_str_sets(out, "/usr/share/git-core/templates"); +#endif +} + +struct git_sysdir__dir { + git_str buf; + int (*guess)(git_str *out); +}; + +static struct git_sysdir__dir git_sysdir__dirs[] = { + { GIT_STR_INIT, git_sysdir_guess_system_dirs }, + { GIT_STR_INIT, git_sysdir_guess_global_dirs }, + { GIT_STR_INIT, git_sysdir_guess_xdg_dirs }, + { GIT_STR_INIT, git_sysdir_guess_programdata_dirs }, + { GIT_STR_INIT, git_sysdir_guess_template_dirs }, + { GIT_STR_INIT, git_sysdir_guess_home_dirs } +}; + +static void git_sysdir_global_shutdown(void) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(git_sysdir__dirs); ++i) + git_str_dispose(&git_sysdir__dirs[i].buf); +} + +int git_sysdir_global_init(void) +{ + size_t i; + int error = 0; + + for (i = 0; !error && i < ARRAY_SIZE(git_sysdir__dirs); i++) + error = git_sysdir__dirs[i].guess(&git_sysdir__dirs[i].buf); + + if (error) + return error; + + return git_runtime_shutdown_register(git_sysdir_global_shutdown); +} + +int git_sysdir_reset(void) +{ + size_t i; + int error = 0; + + for (i = 0; !error && i < ARRAY_SIZE(git_sysdir__dirs); ++i) { + git_str_dispose(&git_sysdir__dirs[i].buf); + error = git_sysdir__dirs[i].guess(&git_sysdir__dirs[i].buf); + } + + return error; +} + +static int git_sysdir_check_selector(git_sysdir_t which) +{ + if (which < ARRAY_SIZE(git_sysdir__dirs)) + return 0; + + git_error_set(GIT_ERROR_INVALID, "config directory selector out of range"); + return -1; +} + + +int git_sysdir_get(const git_str **out, git_sysdir_t which) +{ + GIT_ASSERT_ARG(out); + + *out = NULL; + + GIT_ERROR_CHECK_ERROR(git_sysdir_check_selector(which)); + + *out = &git_sysdir__dirs[which].buf; + return 0; +} + +#define PATH_MAGIC "$PATH" + +int git_sysdir_set(git_sysdir_t which, const char *search_path) +{ + const char *expand_path = NULL; + git_str merge = GIT_STR_INIT; + + GIT_ERROR_CHECK_ERROR(git_sysdir_check_selector(which)); + + if (search_path != NULL) + expand_path = strstr(search_path, PATH_MAGIC); + + /* reset the default if this path has been cleared */ + if (!search_path) + git_sysdir__dirs[which].guess(&git_sysdir__dirs[which].buf); + + /* if $PATH is not referenced, then just set the path */ + if (!expand_path) { + if (search_path) + git_str_sets(&git_sysdir__dirs[which].buf, search_path); + + goto done; + } + + /* otherwise set to join(before $PATH, old value, after $PATH) */ + if (expand_path > search_path) + git_str_set(&merge, search_path, expand_path - search_path); + + if (git_str_len(&git_sysdir__dirs[which].buf)) + git_str_join(&merge, GIT_PATH_LIST_SEPARATOR, + merge.ptr, git_sysdir__dirs[which].buf.ptr); + + expand_path += strlen(PATH_MAGIC); + if (*expand_path) + git_str_join(&merge, GIT_PATH_LIST_SEPARATOR, merge.ptr, expand_path); + + git_str_swap(&git_sysdir__dirs[which].buf, &merge); + git_str_dispose(&merge); + +done: + if (git_str_oom(&git_sysdir__dirs[which].buf)) + return -1; + + return 0; +} + +static int git_sysdir_find_in_dirlist( + git_str *path, + const char *name, + git_sysdir_t which, + const char *label) +{ + size_t len; + const char *scan, *next = NULL; + const git_str *syspath; + + GIT_ERROR_CHECK_ERROR(git_sysdir_get(&syspath, which)); + if (!syspath || !git_str_len(syspath)) + goto done; + + for (scan = git_str_cstr(syspath); scan; scan = next) { + /* find unescaped separator or end of string */ + for (next = scan; *next; ++next) { + if (*next == GIT_PATH_LIST_SEPARATOR && + (next <= scan || next[-1] != '\\')) + break; + } + + len = (size_t)(next - scan); + next = (*next ? next + 1 : NULL); + if (!len) + continue; + + GIT_ERROR_CHECK_ERROR(git_str_set(path, scan, len)); + if (name) + GIT_ERROR_CHECK_ERROR(git_str_joinpath(path, path->ptr, name)); + + if (git_fs_path_exists(path->ptr)) + return 0; + } + +done: + if (name) + git_error_set(GIT_ERROR_OS, "the %s file '%s' doesn't exist", label, name); + else + git_error_set(GIT_ERROR_OS, "the %s directory doesn't exist", label); + git_str_dispose(path); + return GIT_ENOTFOUND; +} + +int git_sysdir_find_system_file(git_str *path, const char *filename) +{ + return git_sysdir_find_in_dirlist( + path, filename, GIT_SYSDIR_SYSTEM, "system"); +} + +int git_sysdir_find_global_file(git_str *path, const char *filename) +{ + return git_sysdir_find_in_dirlist( + path, filename, GIT_SYSDIR_GLOBAL, "global"); +} + +int git_sysdir_find_xdg_file(git_str *path, const char *filename) +{ + return git_sysdir_find_in_dirlist( + path, filename, GIT_SYSDIR_XDG, "global/xdg"); +} + +int git_sysdir_find_programdata_file(git_str *path, const char *filename) +{ + return git_sysdir_find_in_dirlist( + path, filename, GIT_SYSDIR_PROGRAMDATA, "ProgramData"); +} + +int git_sysdir_find_template_dir(git_str *path) +{ + return git_sysdir_find_in_dirlist( + path, NULL, GIT_SYSDIR_TEMPLATE, "template"); +} + +int git_sysdir_find_homedir(git_str *path) +{ + return git_sysdir_find_in_dirlist( + path, NULL, GIT_SYSDIR_HOME, "home directory"); +} + +int git_sysdir_expand_global_file(git_str *path, const char *filename) +{ + int error; + + if ((error = git_sysdir_find_global_file(path, NULL)) == 0) { + if (filename) + error = git_str_joinpath(path, path->ptr, filename); + } + + return error; +} + +int git_sysdir_expand_homedir_file(git_str *path, const char *filename) +{ + int error; + + if ((error = git_sysdir_find_homedir(path)) == 0) { + if (filename) + error = git_str_joinpath(path, path->ptr, filename); + } + + return error; +} diff --git a/src/libgit2/sysdir.h b/src/libgit2/sysdir.h new file mode 100644 index 0000000..03f59e1 --- /dev/null +++ b/src/libgit2/sysdir.h @@ -0,0 +1,145 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_sysdir_h__ +#define INCLUDE_sysdir_h__ + +#include "common.h" + +#include "posix.h" +#include "str.h" + +/** + * Find a "global" file (i.e. one in a user's home directory). + * + * @param path buffer to write the full path into + * @param filename name of file to find in the home directory + * @return 0 if found, GIT_ENOTFOUND if not found, or -1 on other OS error + */ +extern int git_sysdir_find_global_file(git_str *path, const char *filename); + +/** + * Find an "XDG" file (i.e. one in user's XDG config path). + * + * @param path buffer to write the full path into + * @param filename name of file to find in the home directory + * @return 0 if found, GIT_ENOTFOUND if not found, or -1 on other OS error + */ +extern int git_sysdir_find_xdg_file(git_str *path, const char *filename); + +/** + * Find a "system" file (i.e. one shared for all users of the system). + * + * @param path buffer to write the full path into + * @param filename name of file to find in the home directory + * @return 0 if found, GIT_ENOTFOUND if not found, or -1 on other OS error + */ +extern int git_sysdir_find_system_file(git_str *path, const char *filename); + +/** + * Find a "ProgramData" file (i.e. one in %PROGRAMDATA%) + * + * @param path buffer to write the full path into + * @param filename name of file to find in the ProgramData directory + * @return 0 if found, GIT_ENOTFOUND if not found, or -1 on other OS error + */ +extern int git_sysdir_find_programdata_file(git_str *path, const char *filename); + +/** + * Find template directory. + * + * @param path buffer to write the full path into + * @return 0 if found, GIT_ENOTFOUND if not found, or -1 on other OS error + */ +extern int git_sysdir_find_template_dir(git_str *path); + +/** + * Find the home directory. On Windows, this will look at the `HOME`, + * `HOMEPATH`, and `USERPROFILE` environment variables (in that order) + * and return the first path that is set and exists. On other systems, + * this will simply return the contents of the `HOME` environment variable. + * + * @param path buffer to write the full path into + * @return 0 if found, GIT_ENOTFOUND if not found, or -1 on other OS error + */ +extern int git_sysdir_find_homedir(git_str *path); + +/** + * Expand the name of a "global" file -- by default inside the user's + * home directory, but can be overridden by the user configuration. + * Unlike `find_global_file` (above), this makes no attempt to check + * for the existence of the file, and is useful if you want the full + * path regardless of existence. + * + * @param path buffer to write the full path into + * @param filename name of file in the home directory + * @return 0 on success or -1 on error + */ +extern int git_sysdir_expand_global_file(git_str *path, const char *filename); + +/** + * Expand the name of a file in the user's home directory. This + * function makes no attempt to check for the existence of the file, + * and is useful if you want the full path regardless of existence. + * + * @param path buffer to write the full path into + * @param filename name of file in the home directory + * @return 0 on success or -1 on error + */ +extern int git_sysdir_expand_homedir_file(git_str *path, const char *filename); + +typedef enum { + GIT_SYSDIR_SYSTEM = 0, + GIT_SYSDIR_GLOBAL = 1, + GIT_SYSDIR_XDG = 2, + GIT_SYSDIR_PROGRAMDATA = 3, + GIT_SYSDIR_TEMPLATE = 4, + GIT_SYSDIR_HOME = 5, + GIT_SYSDIR__MAX = 6 +} git_sysdir_t; + +/** + * Configures global data for configuration file search paths. + * + * @return 0 on success, <0 on failure + */ +extern int git_sysdir_global_init(void); + +/** + * Get the search path for global/system/xdg files + * + * @param out pointer to git_str containing search path + * @param which which list of paths to return + * @return 0 on success, <0 on failure + */ +extern int git_sysdir_get(const git_str **out, git_sysdir_t which); + +/** + * Set search paths for global/system/xdg files + * + * The first occurrence of the magic string "$PATH" in the new value will + * be replaced with the old value of the search path. + * + * @param which Which search path to modify + * @param paths New search path (separated by GIT_PATH_LIST_SEPARATOR) + * @return 0 on success, <0 on failure (allocation error) + */ +extern int git_sysdir_set(git_sysdir_t which, const char *paths); + +/** + * Reset search paths for global/system/xdg files. + */ +extern int git_sysdir_reset(void); + +#ifdef GIT_WIN32 +/** Sets the registry system dir to a mock; for testing. */ +extern int git_win32__set_registry_system_dir(const wchar_t *mock_sysdir); + +/** Find the given system dir; for testing. */ +extern int git_win32__find_system_dirs(git_str *out, const char *subdir); +#endif + +#endif diff --git a/src/libgit2/tag.c b/src/libgit2/tag.c new file mode 100644 index 0000000..562ec13 --- /dev/null +++ b/src/libgit2/tag.c @@ -0,0 +1,599 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "tag.h" + +#include "commit.h" +#include "signature.h" +#include "wildmatch.h" +#include "git2/object.h" +#include "git2/repository.h" +#include "git2/signature.h" +#include "git2/odb_backend.h" + +void git_tag__free(void *_tag) +{ + git_tag *tag = _tag; + git_signature_free(tag->tagger); + git__free(tag->message); + git__free(tag->tag_name); + git__free(tag); +} + +int git_tag_target(git_object **target, const git_tag *t) +{ + GIT_ASSERT_ARG(t); + return git_object_lookup(target, t->object.repo, &t->target, t->type); +} + +const git_oid *git_tag_target_id(const git_tag *t) +{ + GIT_ASSERT_ARG_WITH_RETVAL(t, NULL); + return &t->target; +} + +git_object_t git_tag_target_type(const git_tag *t) +{ + GIT_ASSERT_ARG_WITH_RETVAL(t, GIT_OBJECT_INVALID); + return t->type; +} + +const char *git_tag_name(const git_tag *t) +{ + GIT_ASSERT_ARG_WITH_RETVAL(t, NULL); + return t->tag_name; +} + +const git_signature *git_tag_tagger(const git_tag *t) +{ + return t->tagger; +} + +const char *git_tag_message(const git_tag *t) +{ + GIT_ASSERT_ARG_WITH_RETVAL(t, NULL); + return t->message; +} + +static int tag_error(const char *str) +{ + git_error_set(GIT_ERROR_TAG, "failed to parse tag: %s", str); + return GIT_EINVALID; +} + +static int tag_parse( + git_tag *tag, + const char *buffer, + const char *buffer_end, + git_oid_t oid_type) +{ + static const char *tag_types[] = { + NULL, "commit\n", "tree\n", "blob\n", "tag\n" + }; + size_t text_len, alloc_len; + const char *search; + unsigned int i; + int error; + + if (git_object__parse_oid_header(&tag->target, + &buffer, buffer_end, "object ", oid_type) < 0) + return tag_error("object field invalid"); + + if (buffer + 5 >= buffer_end) + return tag_error("object too short"); + + if (memcmp(buffer, "type ", 5) != 0) + return tag_error("type field not found"); + buffer += 5; + + tag->type = GIT_OBJECT_INVALID; + + for (i = 1; i < ARRAY_SIZE(tag_types); ++i) { + size_t type_length = strlen(tag_types[i]); + + if (buffer + type_length >= buffer_end) + return tag_error("object too short"); + + if (memcmp(buffer, tag_types[i], type_length) == 0) { + tag->type = i; + buffer += type_length; + break; + } + } + + if (tag->type == GIT_OBJECT_INVALID) + return tag_error("invalid object type"); + + if (buffer + 4 >= buffer_end) + return tag_error("object too short"); + + if (memcmp(buffer, "tag ", 4) != 0) + return tag_error("tag field not found"); + + buffer += 4; + + search = memchr(buffer, '\n', buffer_end - buffer); + if (search == NULL) + return tag_error("object too short"); + + text_len = search - buffer; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, text_len, 1); + tag->tag_name = git__malloc(alloc_len); + GIT_ERROR_CHECK_ALLOC(tag->tag_name); + + memcpy(tag->tag_name, buffer, text_len); + tag->tag_name[text_len] = '\0'; + + buffer = search + 1; + + tag->tagger = NULL; + if (buffer < buffer_end && *buffer != '\n') { + tag->tagger = git__malloc(sizeof(git_signature)); + GIT_ERROR_CHECK_ALLOC(tag->tagger); + + if ((error = git_signature__parse(tag->tagger, &buffer, buffer_end, "tagger ", '\n')) < 0) + return error; + } + + tag->message = NULL; + if (buffer < buffer_end) { + /* If we're not at the end of the header, search for it */ + if(*buffer != '\n') { + search = git__memmem(buffer, buffer_end - buffer, + "\n\n", 2); + if (search) + buffer = search + 1; + else + return tag_error("tag contains no message"); + } + + text_len = buffer_end - ++buffer; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, text_len, 1); + tag->message = git__malloc(alloc_len); + GIT_ERROR_CHECK_ALLOC(tag->message); + + memcpy(tag->message, buffer, text_len); + tag->message[text_len] = '\0'; + } + + return 0; +} + +int git_tag__parse_raw( + void *_tag, + const char *data, + size_t size, + git_oid_t oid_type) +{ + return tag_parse(_tag, data, data + size, oid_type); +} + +int git_tag__parse( + void *_tag, + git_odb_object *odb_obj, + git_oid_t oid_type) +{ + git_tag *tag = _tag; + const char *buffer = git_odb_object_data(odb_obj); + const char *buffer_end = buffer + git_odb_object_size(odb_obj); + + return tag_parse(tag, buffer, buffer_end, oid_type); +} + +static int retrieve_tag_reference( + git_reference **tag_reference_out, + git_str *ref_name_out, + git_repository *repo, + const char *tag_name) +{ + git_reference *tag_ref; + int error; + + *tag_reference_out = NULL; + + if (git_str_joinpath(ref_name_out, GIT_REFS_TAGS_DIR, tag_name) < 0) + return -1; + + error = git_reference_lookup(&tag_ref, repo, ref_name_out->ptr); + if (error < 0) + return error; /* Be it not foundo or corrupted */ + + *tag_reference_out = tag_ref; + + return 0; +} + +static int retrieve_tag_reference_oid( + git_oid *oid, + git_str *ref_name_out, + git_repository *repo, + const char *tag_name) +{ + if (git_str_joinpath(ref_name_out, GIT_REFS_TAGS_DIR, tag_name) < 0) + return -1; + + return git_reference_name_to_id(oid, repo, ref_name_out->ptr); +} + +static int write_tag_annotation( + git_oid *oid, + git_repository *repo, + const char *tag_name, + const git_object *target, + const git_signature *tagger, + const char *message) +{ + git_str tag = GIT_STR_INIT; + git_odb *odb; + + if (git_object__write_oid_header(&tag, "object ", git_object_id(target)) < 0) + goto on_error; + + git_str_printf(&tag, "type %s\n", git_object_type2string(git_object_type(target))); + git_str_printf(&tag, "tag %s\n", tag_name); + git_signature__writebuf(&tag, "tagger ", tagger); + git_str_putc(&tag, '\n'); + + if (git_str_puts(&tag, message) < 0) + goto on_error; + + if (git_repository_odb__weakptr(&odb, repo) < 0) + goto on_error; + + if (git_odb_write(oid, odb, tag.ptr, tag.size, GIT_OBJECT_TAG) < 0) + goto on_error; + + git_str_dispose(&tag); + return 0; + +on_error: + git_str_dispose(&tag); + git_error_set(GIT_ERROR_OBJECT, "failed to create tag annotation"); + return -1; +} + +static bool tag_name_is_valid(const char *tag_name) +{ + /* + * Discourage tag name starting with dash, + * https://github.com/git/git/commit/4f0accd638b8d2 + */ + return tag_name[0] != '-'; +} + +static int git_tag_create__internal( + git_oid *oid, + git_repository *repo, + const char *tag_name, + const git_object *target, + const git_signature *tagger, + const char *message, + int allow_ref_overwrite, + int create_tag_annotation) +{ + git_reference *new_ref = NULL; + git_str ref_name = GIT_STR_INIT; + + int error; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(tag_name); + GIT_ASSERT_ARG(target); + GIT_ASSERT_ARG(!create_tag_annotation || (tagger && message)); + + if (git_object_owner(target) != repo) { + git_error_set(GIT_ERROR_INVALID, "the given target does not belong to this repository"); + return -1; + } + + if (!tag_name_is_valid(tag_name)) { + git_error_set(GIT_ERROR_TAG, "'%s' is not a valid tag name", tag_name); + return -1; + } + + error = retrieve_tag_reference_oid(oid, &ref_name, repo, tag_name); + if (error < 0 && error != GIT_ENOTFOUND) + goto cleanup; + + /** Ensure the tag name doesn't conflict with an already existing + * reference unless overwriting has explicitly been requested **/ + if (error == 0 && !allow_ref_overwrite) { + git_str_dispose(&ref_name); + git_error_set(GIT_ERROR_TAG, "tag already exists"); + return GIT_EEXISTS; + } + + if (create_tag_annotation) { + if (write_tag_annotation(oid, repo, tag_name, target, tagger, message) < 0) { + git_str_dispose(&ref_name); + return -1; + } + } else + git_oid_cpy(oid, git_object_id(target)); + + error = git_reference_create(&new_ref, repo, ref_name.ptr, oid, allow_ref_overwrite, NULL); + +cleanup: + git_reference_free(new_ref); + git_str_dispose(&ref_name); + return error; +} + +int git_tag_create( + git_oid *oid, + git_repository *repo, + const char *tag_name, + const git_object *target, + const git_signature *tagger, + const char *message, + int allow_ref_overwrite) +{ + return git_tag_create__internal(oid, repo, tag_name, target, tagger, message, allow_ref_overwrite, 1); +} + +int git_tag_annotation_create( + git_oid *oid, + git_repository *repo, + const char *tag_name, + const git_object *target, + const git_signature *tagger, + const char *message) +{ + GIT_ASSERT_ARG(oid); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(tag_name); + GIT_ASSERT_ARG(target); + GIT_ASSERT_ARG(tagger); + GIT_ASSERT_ARG(message); + + return write_tag_annotation(oid, repo, tag_name, target, tagger, message); +} + +int git_tag_create_lightweight( + git_oid *oid, + git_repository *repo, + const char *tag_name, + const git_object *target, + int allow_ref_overwrite) +{ + return git_tag_create__internal(oid, repo, tag_name, target, NULL, NULL, allow_ref_overwrite, 0); +} + +int git_tag_create_from_buffer(git_oid *oid, git_repository *repo, const char *buffer, int allow_ref_overwrite) +{ + git_tag tag; + int error; + git_odb *odb; + git_odb_stream *stream; + git_odb_object *target_obj; + + git_reference *new_ref = NULL; + git_str ref_name = GIT_STR_INIT; + + GIT_ASSERT_ARG(oid); + GIT_ASSERT_ARG(buffer); + + memset(&tag, 0, sizeof(tag)); + + if (git_repository_odb__weakptr(&odb, repo) < 0) + return -1; + + /* validate the buffer */ + if (tag_parse(&tag, buffer, buffer + strlen(buffer), repo->oid_type) < 0) + return -1; + + /* validate the target */ + if (git_odb_read(&target_obj, odb, &tag.target) < 0) + goto on_error; + + if (tag.type != target_obj->cached.type) { + git_error_set(GIT_ERROR_TAG, "the type for the given target is invalid"); + goto on_error; + } + + error = retrieve_tag_reference_oid(oid, &ref_name, repo, tag.tag_name); + if (error < 0 && error != GIT_ENOTFOUND) + goto on_error; + + /* We don't need these objects after this */ + git_signature_free(tag.tagger); + git__free(tag.tag_name); + git__free(tag.message); + git_odb_object_free(target_obj); + + /** Ensure the tag name doesn't conflict with an already existing + * reference unless overwriting has explicitly been requested **/ + if (error == 0 && !allow_ref_overwrite) { + git_str_dispose(&ref_name); + git_error_set(GIT_ERROR_TAG, "tag already exists"); + return GIT_EEXISTS; + } + + /* write the buffer */ + if ((error = git_odb_open_wstream( + &stream, odb, strlen(buffer), GIT_OBJECT_TAG)) < 0) { + git_str_dispose(&ref_name); + return error; + } + + if (!(error = git_odb_stream_write(stream, buffer, strlen(buffer)))) + error = git_odb_stream_finalize_write(oid, stream); + + git_odb_stream_free(stream); + + if (error < 0) { + git_str_dispose(&ref_name); + return error; + } + + error = git_reference_create( + &new_ref, repo, ref_name.ptr, oid, allow_ref_overwrite, NULL); + + git_reference_free(new_ref); + git_str_dispose(&ref_name); + + return error; + +on_error: + git_signature_free(tag.tagger); + git__free(tag.tag_name); + git__free(tag.message); + git_odb_object_free(target_obj); + return -1; +} + +int git_tag_delete(git_repository *repo, const char *tag_name) +{ + git_reference *tag_ref; + git_str ref_name = GIT_STR_INIT; + int error; + + error = retrieve_tag_reference(&tag_ref, &ref_name, repo, tag_name); + + git_str_dispose(&ref_name); + + if (error < 0) + return error; + + error = git_reference_delete(tag_ref); + + git_reference_free(tag_ref); + + return error; +} + +typedef struct { + git_repository *repo; + git_tag_foreach_cb cb; + void *cb_data; +} tag_cb_data; + +static int tags_cb(const char *ref, void *data) +{ + int error; + git_oid oid; + tag_cb_data *d = (tag_cb_data *)data; + + if (git__prefixcmp(ref, GIT_REFS_TAGS_DIR) != 0) + return 0; /* no tag */ + + if (!(error = git_reference_name_to_id(&oid, d->repo, ref))) { + if ((error = d->cb(ref, &oid, d->cb_data)) != 0) + git_error_set_after_callback_function(error, "git_tag_foreach"); + } + + return error; +} + +int git_tag_foreach(git_repository *repo, git_tag_foreach_cb cb, void *cb_data) +{ + tag_cb_data data; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(cb); + + data.cb = cb; + data.cb_data = cb_data; + data.repo = repo; + + return git_reference_foreach_name(repo, &tags_cb, &data); +} + +typedef struct { + git_vector *taglist; + const char *pattern; +} tag_filter_data; + +#define GIT_REFS_TAGS_DIR_LEN strlen(GIT_REFS_TAGS_DIR) + +static int tag_list_cb(const char *tag_name, git_oid *oid, void *data) +{ + tag_filter_data *filter = (tag_filter_data *)data; + GIT_UNUSED(oid); + + if (!*filter->pattern || + wildmatch(filter->pattern, tag_name + GIT_REFS_TAGS_DIR_LEN, 0) == 0) + { + char *matched = git__strdup(tag_name + GIT_REFS_TAGS_DIR_LEN); + GIT_ERROR_CHECK_ALLOC(matched); + + return git_vector_insert(filter->taglist, matched); + } + + return 0; +} + +int git_tag_list_match(git_strarray *tag_names, const char *pattern, git_repository *repo) +{ + int error; + tag_filter_data filter; + git_vector taglist; + + GIT_ASSERT_ARG(tag_names); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(pattern); + + if ((error = git_vector_init(&taglist, 8, NULL)) < 0) + return error; + + filter.taglist = &taglist; + filter.pattern = pattern; + + error = git_tag_foreach(repo, &tag_list_cb, (void *)&filter); + + if (error < 0) + git_vector_free(&taglist); + + tag_names->strings = + (char **)git_vector_detach(&tag_names->count, NULL, &taglist); + + return 0; +} + +int git_tag_list(git_strarray *tag_names, git_repository *repo) +{ + return git_tag_list_match(tag_names, "", repo); +} + +int git_tag_peel(git_object **tag_target, const git_tag *tag) +{ + return git_object_peel(tag_target, (const git_object *)tag, GIT_OBJECT_ANY); +} + +int git_tag_name_is_valid(int *valid, const char *name) +{ + git_str ref_name = GIT_STR_INIT; + int error = 0; + + GIT_ASSERT(valid); + + *valid = 0; + + if (!name || !tag_name_is_valid(name)) + goto done; + + if ((error = git_str_puts(&ref_name, GIT_REFS_TAGS_DIR)) < 0 || + (error = git_str_puts(&ref_name, name)) < 0) + goto done; + + error = git_reference_name_is_valid(valid, ref_name.ptr); + +done: + git_str_dispose(&ref_name); + return error; +} + +/* Deprecated Functions */ + +#ifndef GIT_DEPRECATE_HARD +int git_tag_create_frombuffer(git_oid *oid, git_repository *repo, const char *buffer, int allow_ref_overwrite) +{ + return git_tag_create_from_buffer(oid, repo, buffer, allow_ref_overwrite); +} +#endif diff --git a/src/libgit2/tag.h b/src/libgit2/tag.h new file mode 100644 index 0000000..fdaaa46 --- /dev/null +++ b/src/libgit2/tag.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_tag_h__ +#define INCLUDE_tag_h__ + +#include "common.h" + +#include "git2/tag.h" +#include "repository.h" +#include "odb.h" + +struct git_tag { + git_object object; + + git_oid target; + git_object_t type; + + char *tag_name; + git_signature *tagger; + char *message; +}; + +void git_tag__free(void *tag); +int git_tag__parse(void *tag, git_odb_object *obj, git_oid_t oid_type); +int git_tag__parse_raw(void *tag, const char *data, size_t size, git_oid_t oid_type); + +#endif diff --git a/src/libgit2/threadstate.c b/src/libgit2/threadstate.c new file mode 100644 index 0000000..ed9bb9b --- /dev/null +++ b/src/libgit2/threadstate.c @@ -0,0 +1,97 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "threadstate.h" +#include "runtime.h" + +/** + * Handle the thread-local state + * + * `git_threadstate_global_init` will be called as part + * of `git_libgit2_init` (which itself must be called + * before calling any other function in the library). + * + * This function allocates a TLS index to store the per- + * thread state. + * + * Any internal method that requires thread-local state + * will then call `git_threadstate_get()` which returns a + * pointer to the thread-local state structure; this + * structure is lazily allocated on each thread. + * + * This mechanism will register a shutdown handler + * (`git_threadstate_global_shutdown`) which will free the + * TLS index. This shutdown handler will be called by + * `git_libgit2_shutdown`. + */ + +static git_tlsdata_key tls_key; + +static void threadstate_dispose(git_threadstate *threadstate) +{ + if (!threadstate) + return; + + if (threadstate->error_t.message != git_str__initstr) + git__free(threadstate->error_t.message); + threadstate->error_t.message = NULL; +} + +static void GIT_SYSTEM_CALL threadstate_free(void *threadstate) +{ + threadstate_dispose(threadstate); + git__free(threadstate); +} + +static void git_threadstate_global_shutdown(void) +{ + git_threadstate *threadstate; + + threadstate = git_tlsdata_get(tls_key); + git_tlsdata_set(tls_key, NULL); + + threadstate_dispose(threadstate); + git__free(threadstate); + + git_tlsdata_dispose(tls_key); +} + +int git_threadstate_global_init(void) +{ + if (git_tlsdata_init(&tls_key, &threadstate_free) != 0) + return -1; + + return git_runtime_shutdown_register(git_threadstate_global_shutdown); +} + +git_threadstate *git_threadstate_get(void) +{ + git_threadstate *threadstate; + + if ((threadstate = git_tlsdata_get(tls_key)) != NULL) + return threadstate; + + /* + * Avoid git__malloc here, since if it fails, it sets an error + * message, which requires thread state, which would allocate + * here, which would fail, which would set an error message... + */ + + if ((threadstate = git__allocator.gmalloc(sizeof(git_threadstate), + __FILE__, __LINE__)) == NULL) + return NULL; + + memset(threadstate, 0, sizeof(git_threadstate)); + + if (git_str_init(&threadstate->error_buf, 0) < 0) { + git__allocator.gfree(threadstate); + return NULL; + } + + git_tlsdata_set(tls_key, threadstate); + return threadstate; +} diff --git a/src/libgit2/threadstate.h b/src/libgit2/threadstate.h new file mode 100644 index 0000000..6ef0419 --- /dev/null +++ b/src/libgit2/threadstate.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_threadstate_h__ +#define INCLUDE_threadstate_h__ + +#include "common.h" + +typedef struct { + git_error *last_error; + git_error error_t; + git_str error_buf; + char oid_fmt[GIT_OID_MAX_HEXSIZE+1]; +} git_threadstate; + +extern int git_threadstate_global_init(void); +extern git_threadstate *git_threadstate_get(void); + +#endif diff --git a/src/libgit2/trace.c b/src/libgit2/trace.c new file mode 100644 index 0000000..b0c56c4 --- /dev/null +++ b/src/libgit2/trace.c @@ -0,0 +1,25 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "trace.h" + +#include "str.h" +#include "runtime.h" +#include "git2/trace.h" + +struct git_trace_data git_trace__data = {0}; + +int git_trace_set(git_trace_level_t level, git_trace_cb callback) +{ + GIT_ASSERT_ARG(level == 0 || callback != NULL); + + git_trace__data.level = level; + git_trace__data.callback = callback; + GIT_MEMORY_BARRIER; + + return 0; +} diff --git a/src/libgit2/trace.h b/src/libgit2/trace.h new file mode 100644 index 0000000..239928d --- /dev/null +++ b/src/libgit2/trace.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_trace_h__ +#define INCLUDE_trace_h__ + +#include "common.h" + +#include +#include "str.h" + +struct git_trace_data { + git_trace_level_t level; + git_trace_cb callback; +}; + +extern struct git_trace_data git_trace__data; + +GIT_INLINE(void) git_trace__write_fmt( + git_trace_level_t level, + const char *fmt, + va_list ap) +{ + git_trace_cb callback = git_trace__data.callback; + git_str message = GIT_STR_INIT; + + git_str_vprintf(&message, fmt, ap); + + callback(level, git_str_cstr(&message)); + + git_str_dispose(&message); +} + +#define git_trace_level() (git_trace__data.level) + +GIT_INLINE(void) git_trace(git_trace_level_t level, const char *fmt, ...) +{ + if (git_trace__data.level >= level && + git_trace__data.callback != NULL) { + va_list ap; + + va_start(ap, fmt); + git_trace__write_fmt(level, fmt, ap); + va_end(ap); + } +} + +#endif diff --git a/src/libgit2/trailer.c b/src/libgit2/trailer.c new file mode 100644 index 0000000..4761c99 --- /dev/null +++ b/src/libgit2/trailer.c @@ -0,0 +1,430 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#include "array.h" +#include "common.h" +#include "git2/message.h" + +#include +#include +#include + +#define COMMENT_LINE_CHAR '#' +#define TRAILER_SEPARATORS ":" + +static const char *const git_generated_prefixes[] = { + "Signed-off-by: ", + "(cherry picked from commit ", + NULL +}; + +static int is_blank_line(const char *str) +{ + const char *s = str; + while (*s && *s != '\n' && isspace(*s)) + s++; + return !*s || *s == '\n'; +} + +static const char *next_line(const char *str) +{ + const char *nl = strchr(str, '\n'); + + if (nl) { + return nl + 1; + } else { + /* return pointer to the NUL terminator: */ + return str + strlen(str); + } +} + +/* + * Return the position of the start of the last line. If len is 0, return 0. + */ +static bool last_line(size_t *out, const char *buf, size_t len) +{ + size_t i; + + *out = 0; + + if (len == 0) + return false; + if (len == 1) + return true; + + /* + * Skip the last character (in addition to the null terminator), + * because if the last character is a newline, it is considered as part + * of the last line anyway. + */ + i = len - 2; + + for (; i > 0; i--) { + if (buf[i] == '\n') { + *out = i + 1; + return true; + } + } + return true; +} + +/* + * If the given line is of the form + * "..." or "...", sets out + * to the location of the separator and returns true. Otherwise, returns + * false. The optional whitespace is allowed there primarily to allow things + * like "Bug #43" where is "Bug" and is "#". + * + * The separator-starts-line case (in which this function returns true and + * sets out to 0) is distinguished from the non-well-formed-line case (in + * which this function returns false) because some callers of this function + * need such a distinction. + */ +static bool find_separator(size_t *out, const char *line, const char *separators) +{ + int whitespace_found = 0; + const char *c; + for (c = line; *c; c++) { + if (strchr(separators, *c)) { + *out = c - line; + return true; + } + + if (!whitespace_found && (isalnum(*c) || *c == '-')) + continue; + if (c != line && (*c == ' ' || *c == '\t')) { + whitespace_found = 1; + continue; + } + break; + } + return false; +} + +/* + * Inspect the given string and determine the true "end" of the log message, in + * order to find where to put a new Signed-off-by: line. Ignored are + * trailing comment lines and blank lines. To support "git commit -s + * --amend" on an existing commit, we also ignore "Conflicts:". To + * support "git commit -v", we truncate at cut lines. + * + * Returns the number of bytes from the tail to ignore, to be fed as + * the second parameter to append_signoff(). + */ +static size_t ignore_non_trailer(const char *buf, size_t len) +{ + size_t boc = 0, bol = 0; + int in_old_conflicts_block = 0; + size_t cutoff = len; + + while (bol < cutoff) { + const char *next_line = memchr(buf + bol, '\n', len - bol); + + if (!next_line) + next_line = buf + len; + else + next_line++; + + if (buf[bol] == COMMENT_LINE_CHAR || buf[bol] == '\n') { + /* is this the first of the run of comments? */ + if (!boc) + boc = bol; + /* otherwise, it is just continuing */ + } else if (git__prefixcmp(buf + bol, "Conflicts:\n") == 0) { + in_old_conflicts_block = 1; + if (!boc) + boc = bol; + } else if (in_old_conflicts_block && buf[bol] == '\t') { + ; /* a pathname in the conflicts block */ + } else if (boc) { + /* the previous was not trailing comment */ + boc = 0; + in_old_conflicts_block = 0; + } + bol = next_line - buf; + } + return boc ? len - boc : len - cutoff; +} + +/* + * Return the position of the start of the patch or the length of str if there + * is no patch in the message. + */ +static size_t find_patch_start(const char *str) +{ + const char *s; + + for (s = str; *s; s = next_line(s)) { + if (git__prefixcmp(s, "---") == 0) + return s - str; + } + + return s - str; +} + +/* + * Return the position of the first trailer line or len if there are no + * trailers. + */ +static size_t find_trailer_start(const char *buf, size_t len) +{ + const char *s; + size_t end_of_title, l; + int only_spaces = 1; + int recognized_prefix = 0, trailer_lines = 0, non_trailer_lines = 0; + /* + * Number of possible continuation lines encountered. This will be + * reset to 0 if we encounter a trailer (since those lines are to be + * considered continuations of that trailer), and added to + * non_trailer_lines if we encounter a non-trailer (since those lines + * are to be considered non-trailers). + */ + int possible_continuation_lines = 0; + + /* The first paragraph is the title and cannot be trailers */ + for (s = buf; s < buf + len; s = next_line(s)) { + if (s[0] == COMMENT_LINE_CHAR) + continue; + if (is_blank_line(s)) + break; + } + end_of_title = s - buf; + + /* + * Get the start of the trailers by looking starting from the end for a + * blank line before a set of non-blank lines that (i) are all + * trailers, or (ii) contains at least one Git-generated trailer and + * consists of at least 25% trailers. + */ + l = len; + while (last_line(&l, buf, l) && l >= end_of_title) { + const char *bol = buf + l; + const char *const *p; + size_t separator_pos = 0; + + if (bol[0] == COMMENT_LINE_CHAR) { + non_trailer_lines += possible_continuation_lines; + possible_continuation_lines = 0; + continue; + } + if (is_blank_line(bol)) { + if (only_spaces) + continue; + non_trailer_lines += possible_continuation_lines; + if (recognized_prefix && + trailer_lines * 3 >= non_trailer_lines) + return next_line(bol) - buf; + else if (trailer_lines && !non_trailer_lines) + return next_line(bol) - buf; + return len; + } + only_spaces = 0; + + for (p = git_generated_prefixes; *p; p++) { + if (git__prefixcmp(bol, *p) == 0) { + trailer_lines++; + possible_continuation_lines = 0; + recognized_prefix = 1; + goto continue_outer_loop; + } + } + + find_separator(&separator_pos, bol, TRAILER_SEPARATORS); + if (separator_pos >= 1 && !isspace(bol[0])) { + trailer_lines++; + possible_continuation_lines = 0; + if (recognized_prefix) + continue; + } else if (isspace(bol[0])) + possible_continuation_lines++; + else { + non_trailer_lines++; + non_trailer_lines += possible_continuation_lines; + possible_continuation_lines = 0; + } +continue_outer_loop: + ; + } + + return len; +} + +/* Return the position of the end of the trailers. */ +static size_t find_trailer_end(const char *buf, size_t len) +{ + return len - ignore_non_trailer(buf, len); +} + +static char *extract_trailer_block(const char *message, size_t *len) +{ + size_t patch_start = find_patch_start(message); + size_t trailer_end = find_trailer_end(message, patch_start); + size_t trailer_start = find_trailer_start(message, trailer_end); + + size_t trailer_len = trailer_end - trailer_start; + + char *buffer = git__malloc(trailer_len + 1); + if (buffer == NULL) + return NULL; + + memcpy(buffer, message + trailer_start, trailer_len); + buffer[trailer_len] = 0; + + *len = trailer_len; + + return buffer; +} + +enum trailer_state { + S_START = 0, + S_KEY = 1, + S_KEY_WS = 2, + S_SEP_WS = 3, + S_VALUE = 4, + S_VALUE_NL = 5, + S_VALUE_END = 6, + S_IGNORE = 7 +}; + +#define NEXT(st) { state = (st); ptr++; continue; } +#define GOTO(st) { state = (st); continue; } + +typedef git_array_t(git_message_trailer) git_array_trailer_t; + +int git_message_trailers(git_message_trailer_array *trailer_arr, const char *message) +{ + enum trailer_state state = S_START; + int rc = 0; + char *ptr; + char *key = NULL; + char *value = NULL; + git_array_trailer_t arr = GIT_ARRAY_INIT; + + size_t trailer_len; + char *trailer = extract_trailer_block(message, &trailer_len); + if (trailer == NULL) + return -1; + + for (ptr = trailer;;) { + switch (state) { + case S_START: { + if (*ptr == 0) { + goto ret; + } + + key = ptr; + GOTO(S_KEY); + } + case S_KEY: { + if (*ptr == 0) { + goto ret; + } + + if (isalnum(*ptr) || *ptr == '-') { + /* legal key character */ + NEXT(S_KEY); + } + + if (*ptr == ' ' || *ptr == '\t') { + /* optional whitespace before separator */ + *ptr = 0; + NEXT(S_KEY_WS); + } + + if (strchr(TRAILER_SEPARATORS, *ptr)) { + *ptr = 0; + NEXT(S_SEP_WS); + } + + /* illegal character */ + GOTO(S_IGNORE); + } + case S_KEY_WS: { + if (*ptr == 0) { + goto ret; + } + + if (*ptr == ' ' || *ptr == '\t') { + NEXT(S_KEY_WS); + } + + if (strchr(TRAILER_SEPARATORS, *ptr)) { + NEXT(S_SEP_WS); + } + + /* illegal character */ + GOTO(S_IGNORE); + } + case S_SEP_WS: { + if (*ptr == 0) { + goto ret; + } + + if (*ptr == ' ' || *ptr == '\t') { + NEXT(S_SEP_WS); + } + + value = ptr; + NEXT(S_VALUE); + } + case S_VALUE: { + if (*ptr == 0) { + GOTO(S_VALUE_END); + } + + if (*ptr == '\n') { + NEXT(S_VALUE_NL); + } + + NEXT(S_VALUE); + } + case S_VALUE_NL: { + if (*ptr == ' ') { + /* continuation; */ + NEXT(S_VALUE); + } + + ptr[-1] = 0; + GOTO(S_VALUE_END); + } + case S_VALUE_END: { + git_message_trailer *t = git_array_alloc(arr); + + t->key = key; + t->value = value; + + key = NULL; + value = NULL; + + GOTO(S_START); + } + case S_IGNORE: { + if (*ptr == 0) { + goto ret; + } + + if (*ptr == '\n') { + NEXT(S_START); + } + + NEXT(S_IGNORE); + } + } + } + +ret: + trailer_arr->_trailer_block = trailer; + trailer_arr->trailers = arr.ptr; + trailer_arr->count = arr.size; + + return rc; +} + +void git_message_trailer_array_free(git_message_trailer_array *arr) +{ + git__free(arr->_trailer_block); + git__free(arr->trailers); +} diff --git a/src/libgit2/transaction.c b/src/libgit2/transaction.c new file mode 100644 index 0000000..ccffa99 --- /dev/null +++ b/src/libgit2/transaction.c @@ -0,0 +1,395 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "transaction.h" + +#include "repository.h" +#include "strmap.h" +#include "refdb.h" +#include "pool.h" +#include "reflog.h" +#include "signature.h" +#include "config.h" + +#include "git2/transaction.h" +#include "git2/signature.h" +#include "git2/sys/refs.h" +#include "git2/sys/refdb_backend.h" + +typedef enum { + TRANSACTION_NONE, + TRANSACTION_REFS, + TRANSACTION_CONFIG +} transaction_t; + +typedef struct { + const char *name; + void *payload; + + git_reference_t ref_type; + union { + git_oid id; + char *symbolic; + } target; + git_reflog *reflog; + + const char *message; + git_signature *sig; + + unsigned int committed :1, + remove :1; +} transaction_node; + +struct git_transaction { + transaction_t type; + git_repository *repo; + git_refdb *db; + git_config *cfg; + + git_strmap *locks; + git_pool pool; +}; + +int git_transaction_config_new(git_transaction **out, git_config *cfg) +{ + git_transaction *tx; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(cfg); + + tx = git__calloc(1, sizeof(git_transaction)); + GIT_ERROR_CHECK_ALLOC(tx); + + tx->type = TRANSACTION_CONFIG; + tx->cfg = cfg; + *out = tx; + return 0; +} + +int git_transaction_new(git_transaction **out, git_repository *repo) +{ + int error; + git_pool pool; + git_transaction *tx = NULL; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + + if ((error = git_pool_init(&pool, 1)) < 0) + goto on_error; + + tx = git_pool_mallocz(&pool, sizeof(git_transaction)); + if (!tx) { + error = -1; + goto on_error; + } + + if ((error = git_strmap_new(&tx->locks)) < 0) { + error = -1; + goto on_error; + } + + if ((error = git_repository_refdb(&tx->db, repo)) < 0) + goto on_error; + + tx->type = TRANSACTION_REFS; + memcpy(&tx->pool, &pool, sizeof(git_pool)); + tx->repo = repo; + *out = tx; + return 0; + +on_error: + git_pool_clear(&pool); + return error; +} + +int git_transaction_lock_ref(git_transaction *tx, const char *refname) +{ + int error; + transaction_node *node; + + GIT_ASSERT_ARG(tx); + GIT_ASSERT_ARG(refname); + + node = git_pool_mallocz(&tx->pool, sizeof(transaction_node)); + GIT_ERROR_CHECK_ALLOC(node); + + node->name = git_pool_strdup(&tx->pool, refname); + GIT_ERROR_CHECK_ALLOC(node->name); + + if ((error = git_refdb_lock(&node->payload, tx->db, refname)) < 0) + return error; + + if ((error = git_strmap_set(tx->locks, node->name, node)) < 0) + goto cleanup; + + return 0; + +cleanup: + git_refdb_unlock(tx->db, node->payload, false, false, NULL, NULL, NULL); + + return error; +} + +static int find_locked(transaction_node **out, git_transaction *tx, const char *refname) +{ + transaction_node *node; + + if ((node = git_strmap_get(tx->locks, refname)) == NULL) { + git_error_set(GIT_ERROR_REFERENCE, "the specified reference is not locked"); + return GIT_ENOTFOUND; + } + + *out = node; + return 0; +} + +static int copy_common(transaction_node *node, git_transaction *tx, const git_signature *sig, const char *msg) +{ + if (sig && git_signature__pdup(&node->sig, sig, &tx->pool) < 0) + return -1; + + if (!node->sig) { + git_signature *tmp; + int error; + + if (git_reference__log_signature(&tmp, tx->repo) < 0) + return -1; + + /* make sure the sig we use is in our pool */ + error = git_signature__pdup(&node->sig, tmp, &tx->pool); + git_signature_free(tmp); + if (error < 0) + return error; + } + + if (msg) { + node->message = git_pool_strdup(&tx->pool, msg); + GIT_ERROR_CHECK_ALLOC(node->message); + } + + return 0; +} + +int git_transaction_set_target(git_transaction *tx, const char *refname, const git_oid *target, const git_signature *sig, const char *msg) +{ + int error; + transaction_node *node; + + GIT_ASSERT_ARG(tx); + GIT_ASSERT_ARG(refname); + GIT_ASSERT_ARG(target); + + if ((error = find_locked(&node, tx, refname)) < 0) + return error; + + if ((error = copy_common(node, tx, sig, msg)) < 0) + return error; + + git_oid_cpy(&node->target.id, target); + node->ref_type = GIT_REFERENCE_DIRECT; + + return 0; +} + +int git_transaction_set_symbolic_target(git_transaction *tx, const char *refname, const char *target, const git_signature *sig, const char *msg) +{ + int error; + transaction_node *node; + + GIT_ASSERT_ARG(tx); + GIT_ASSERT_ARG(refname); + GIT_ASSERT_ARG(target); + + if ((error = find_locked(&node, tx, refname)) < 0) + return error; + + if ((error = copy_common(node, tx, sig, msg)) < 0) + return error; + + node->target.symbolic = git_pool_strdup(&tx->pool, target); + GIT_ERROR_CHECK_ALLOC(node->target.symbolic); + node->ref_type = GIT_REFERENCE_SYMBOLIC; + + return 0; +} + +int git_transaction_remove(git_transaction *tx, const char *refname) +{ + int error; + transaction_node *node; + + if ((error = find_locked(&node, tx, refname)) < 0) + return error; + + node->remove = true; + node->ref_type = GIT_REFERENCE_DIRECT; /* the id will be ignored */ + + return 0; +} + +static int dup_reflog(git_reflog **out, const git_reflog *in, git_pool *pool) +{ + git_reflog *reflog; + git_reflog_entry *entries; + size_t len, i; + + reflog = git_pool_mallocz(pool, sizeof(git_reflog)); + GIT_ERROR_CHECK_ALLOC(reflog); + + reflog->ref_name = git_pool_strdup(pool, in->ref_name); + GIT_ERROR_CHECK_ALLOC(reflog->ref_name); + + len = in->entries.length; + reflog->entries.length = len; + reflog->entries.contents = git_pool_mallocz(pool, len * sizeof(void *)); + GIT_ERROR_CHECK_ALLOC(reflog->entries.contents); + + entries = git_pool_mallocz(pool, len * sizeof(git_reflog_entry)); + GIT_ERROR_CHECK_ALLOC(entries); + + for (i = 0; i < len; i++) { + const git_reflog_entry *src; + git_reflog_entry *tgt; + + tgt = &entries[i]; + reflog->entries.contents[i] = tgt; + + src = git_vector_get(&in->entries, i); + git_oid_cpy(&tgt->oid_old, &src->oid_old); + git_oid_cpy(&tgt->oid_cur, &src->oid_cur); + + tgt->msg = git_pool_strdup(pool, src->msg); + GIT_ERROR_CHECK_ALLOC(tgt->msg); + + if (git_signature__pdup(&tgt->committer, src->committer, pool) < 0) + return -1; + } + + + *out = reflog; + return 0; +} + +int git_transaction_set_reflog(git_transaction *tx, const char *refname, const git_reflog *reflog) +{ + int error; + transaction_node *node; + + GIT_ASSERT_ARG(tx); + GIT_ASSERT_ARG(refname); + GIT_ASSERT_ARG(reflog); + + if ((error = find_locked(&node, tx, refname)) < 0) + return error; + + if ((error = dup_reflog(&node->reflog, reflog, &tx->pool)) < 0) + return error; + + return 0; +} + +static int update_target(git_refdb *db, transaction_node *node) +{ + git_reference *ref; + int error, update_reflog; + + if (node->ref_type == GIT_REFERENCE_DIRECT) { + ref = git_reference__alloc(node->name, &node->target.id, NULL); + } else if (node->ref_type == GIT_REFERENCE_SYMBOLIC) { + ref = git_reference__alloc_symbolic(node->name, node->target.symbolic); + } else { + abort(); + } + + GIT_ERROR_CHECK_ALLOC(ref); + update_reflog = node->reflog == NULL; + + if (node->remove) { + error = git_refdb_unlock(db, node->payload, 2, false, ref, NULL, NULL); + } else if (node->ref_type == GIT_REFERENCE_DIRECT) { + error = git_refdb_unlock(db, node->payload, true, update_reflog, ref, node->sig, node->message); + } else if (node->ref_type == GIT_REFERENCE_SYMBOLIC) { + error = git_refdb_unlock(db, node->payload, true, update_reflog, ref, node->sig, node->message); + } else { + abort(); + } + + git_reference_free(ref); + node->committed = true; + + return error; +} + +int git_transaction_commit(git_transaction *tx) +{ + transaction_node *node; + int error = 0; + + GIT_ASSERT_ARG(tx); + + if (tx->type == TRANSACTION_CONFIG) { + error = git_config_unlock(tx->cfg, true); + tx->cfg = NULL; + + return error; + } + + git_strmap_foreach_value(tx->locks, node, { + if (node->reflog) { + if ((error = tx->db->backend->reflog_write(tx->db->backend, node->reflog)) < 0) + return error; + } + + if (node->ref_type == GIT_REFERENCE_INVALID) { + /* ref was locked but not modified */ + if ((error = git_refdb_unlock(tx->db, node->payload, false, false, NULL, NULL, NULL)) < 0) { + return error; + } + node->committed = true; + } else { + if ((error = update_target(tx->db, node)) < 0) + return error; + } + }); + + return 0; +} + +void git_transaction_free(git_transaction *tx) +{ + transaction_node *node; + git_pool pool; + + if (!tx) + return; + + if (tx->type == TRANSACTION_CONFIG) { + if (tx->cfg) { + git_config_unlock(tx->cfg, false); + git_config_free(tx->cfg); + } + + git__free(tx); + return; + } + + /* start by unlocking the ones we've left hanging, if any */ + git_strmap_foreach_value(tx->locks, node, { + if (node->committed) + continue; + + git_refdb_unlock(tx->db, node->payload, false, false, NULL, NULL, NULL); + }); + + git_refdb_free(tx->db); + git_strmap_free(tx->locks); + + /* tx is inside the pool, so we need to extract the data */ + memcpy(&pool, &tx->pool, sizeof(git_pool)); + git_pool_clear(&pool); +} diff --git a/src/libgit2/transaction.h b/src/libgit2/transaction.h new file mode 100644 index 0000000..780c068 --- /dev/null +++ b/src/libgit2/transaction.h @@ -0,0 +1,14 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_transaction_h__ +#define INCLUDE_transaction_h__ + +#include "common.h" + +int git_transaction_config_new(git_transaction **out, git_config *cfg); + +#endif diff --git a/src/libgit2/transport.c b/src/libgit2/transport.c new file mode 100644 index 0000000..640ccac --- /dev/null +++ b/src/libgit2/transport.c @@ -0,0 +1,222 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "git2/types.h" +#include "git2/remote.h" +#include "git2/net.h" +#include "git2/transport.h" +#include "git2/sys/transport.h" +#include "fs_path.h" + +typedef struct transport_definition { + char *prefix; + git_transport_cb fn; + void *param; +} transport_definition; + +static git_smart_subtransport_definition http_subtransport_definition = { git_smart_subtransport_http, 1, NULL }; +static git_smart_subtransport_definition git_subtransport_definition = { git_smart_subtransport_git, 0, NULL }; +#ifdef GIT_SSH +static git_smart_subtransport_definition ssh_subtransport_definition = { git_smart_subtransport_ssh, 0, NULL }; +#endif + +static transport_definition local_transport_definition = { "file://", git_transport_local, NULL }; + +static transport_definition transports[] = { + { "git://", git_transport_smart, &git_subtransport_definition }, + { "http://", git_transport_smart, &http_subtransport_definition }, + { "https://", git_transport_smart, &http_subtransport_definition }, + { "file://", git_transport_local, NULL }, +#ifdef GIT_SSH + { "ssh://", git_transport_smart, &ssh_subtransport_definition }, + { "ssh+git://", git_transport_smart, &ssh_subtransport_definition }, + { "git+ssh://", git_transport_smart, &ssh_subtransport_definition }, +#endif + { NULL, 0, 0 } +}; + +static git_vector custom_transports = GIT_VECTOR_INIT; + +#define GIT_TRANSPORT_COUNT (sizeof(transports)/sizeof(transports[0])) - 1 + +static transport_definition * transport_find_by_url(const char *url) +{ + size_t i = 0; + transport_definition *d; + + /* Find a user transport who wants to deal with this URI */ + git_vector_foreach(&custom_transports, i, d) { + if (strncasecmp(url, d->prefix, strlen(d->prefix)) == 0) { + return d; + } + } + + /* Find a system transport for this URI */ + for (i = 0; i < GIT_TRANSPORT_COUNT; ++i) { + d = &transports[i]; + + if (strncasecmp(url, d->prefix, strlen(d->prefix)) == 0) { + return d; + } + } + + return NULL; +} + +static int transport_find_fn( + git_transport_cb *out, + const char *url, + void **param) +{ + transport_definition *definition = transport_find_by_url(url); + +#ifdef GIT_WIN32 + /* On Windows, it might not be possible to discern between absolute local + * and ssh paths - first check if this is a valid local path that points + * to a directory and if so assume local path, else assume SSH */ + + /* Check to see if the path points to a file on the local file system */ + if (!definition && git_fs_path_exists(url) && git_fs_path_isdir(url)) + definition = &local_transport_definition; +#endif + + /* For other systems, perform the SSH check first, to avoid going to the + * filesystem if it is not necessary */ + + /* It could be a SSH remote path. Check to see if there's a : */ + if (!definition && strrchr(url, ':')) { + /* re-search transports again with ssh:// as url + * so that we can find a third party ssh transport */ + definition = transport_find_by_url("ssh://"); + } + +#ifndef GIT_WIN32 + /* Check to see if the path points to a file on the local file system */ + if (!definition && git_fs_path_exists(url) && git_fs_path_isdir(url)) + definition = &local_transport_definition; +#endif + + if (!definition) + return GIT_ENOTFOUND; + + *out = definition->fn; + *param = definition->param; + + return 0; +} + +/************** + * Public API * + **************/ + +int git_transport_new(git_transport **out, git_remote *owner, const char *url) +{ + git_transport_cb fn; + git_transport *transport; + void *param; + int error; + + if ((error = transport_find_fn(&fn, url, ¶m)) == GIT_ENOTFOUND) { + git_error_set(GIT_ERROR_NET, "unsupported URL protocol"); + return -1; + } else if (error < 0) + return error; + + if ((error = fn(&transport, owner, param)) < 0) + return error; + + GIT_ERROR_CHECK_VERSION(transport, GIT_TRANSPORT_VERSION, "git_transport"); + + *out = transport; + + return 0; +} + +int git_transport_register( + const char *scheme, + git_transport_cb cb, + void *param) +{ + git_str prefix = GIT_STR_INIT; + transport_definition *d, *definition = NULL; + size_t i; + int error = 0; + + GIT_ASSERT_ARG(scheme); + GIT_ASSERT_ARG(cb); + + if ((error = git_str_printf(&prefix, "%s://", scheme)) < 0) + goto on_error; + + git_vector_foreach(&custom_transports, i, d) { + if (strcasecmp(d->prefix, prefix.ptr) == 0) { + error = GIT_EEXISTS; + goto on_error; + } + } + + definition = git__calloc(1, sizeof(transport_definition)); + GIT_ERROR_CHECK_ALLOC(definition); + + definition->prefix = git_str_detach(&prefix); + definition->fn = cb; + definition->param = param; + + if (git_vector_insert(&custom_transports, definition) < 0) + goto on_error; + + return 0; + +on_error: + git_str_dispose(&prefix); + git__free(definition); + return error; +} + +int git_transport_unregister(const char *scheme) +{ + git_str prefix = GIT_STR_INIT; + transport_definition *d; + size_t i; + int error = 0; + + GIT_ASSERT_ARG(scheme); + + if ((error = git_str_printf(&prefix, "%s://", scheme)) < 0) + goto done; + + git_vector_foreach(&custom_transports, i, d) { + if (strcasecmp(d->prefix, prefix.ptr) == 0) { + if ((error = git_vector_remove(&custom_transports, i)) < 0) + goto done; + + git__free(d->prefix); + git__free(d); + + if (!custom_transports.length) + git_vector_free(&custom_transports); + + error = 0; + goto done; + } + } + + error = GIT_ENOTFOUND; + +done: + git_str_dispose(&prefix); + return error; +} + +int git_transport_init(git_transport *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_transport, GIT_TRANSPORT_INIT); + return 0; +} diff --git a/src/libgit2/transports/auth.c b/src/libgit2/transports/auth.c new file mode 100644 index 0000000..90b6b12 --- /dev/null +++ b/src/libgit2/transports/auth.c @@ -0,0 +1,74 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "auth.h" + +#include "git2/sys/credential.h" + +static int basic_next_token( + git_str *out, + git_http_auth_context *ctx, + git_credential *c) +{ + git_credential_userpass_plaintext *cred; + git_str raw = GIT_STR_INIT; + int error = GIT_EAUTH; + + GIT_UNUSED(ctx); + + if (c->credtype != GIT_CREDENTIAL_USERPASS_PLAINTEXT) { + git_error_set(GIT_ERROR_INVALID, "invalid credential type for basic auth"); + goto on_error; + } + + cred = (git_credential_userpass_plaintext *)c; + + git_str_printf(&raw, "%s:%s", cred->username, cred->password); + + if (git_str_oom(&raw) || + git_str_puts(out, "Basic ") < 0 || + git_str_encode_base64(out, git_str_cstr(&raw), raw.size) < 0) + goto on_error; + + error = 0; + +on_error: + if (raw.size) + git__memzero(raw.ptr, raw.size); + + git_str_dispose(&raw); + return error; +} + +static git_http_auth_context basic_context = { + GIT_HTTP_AUTH_BASIC, + GIT_CREDENTIAL_USERPASS_PLAINTEXT, + 0, + NULL, + basic_next_token, + NULL, + NULL +}; + +int git_http_auth_basic( + git_http_auth_context **out, const git_net_url *url) +{ + GIT_UNUSED(url); + + *out = &basic_context; + return 0; +} + +int git_http_auth_dummy( + git_http_auth_context **out, const git_net_url *url) +{ + GIT_UNUSED(url); + + *out = NULL; + return GIT_PASSTHROUGH; +} + diff --git a/src/libgit2/transports/auth.h b/src/libgit2/transports/auth.h new file mode 100644 index 0000000..9f6f8fd --- /dev/null +++ b/src/libgit2/transports/auth.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_transports_auth_h__ +#define INCLUDE_transports_auth_h__ + +#include "common.h" +#include "net.h" + +typedef enum { + GIT_HTTP_AUTH_BASIC = 1, + GIT_HTTP_AUTH_NEGOTIATE = 2, + GIT_HTTP_AUTH_NTLM = 4 +} git_http_auth_t; + +typedef struct git_http_auth_context git_http_auth_context; + +struct git_http_auth_context { + /** Type of scheme */ + git_http_auth_t type; + + /** Supported credentials */ + git_credential_t credtypes; + + /** Connection affinity or request affinity */ + unsigned connection_affinity : 1; + + /** Sets the challenge on the authentication context */ + int (*set_challenge)(git_http_auth_context *ctx, const char *challenge); + + /** Gets the next authentication token from the context */ + int (*next_token)(git_str *out, git_http_auth_context *ctx, git_credential *cred); + + /** Examines if all tokens have been presented. */ + int (*is_complete)(git_http_auth_context *ctx); + + /** Frees the authentication context */ + void (*free)(git_http_auth_context *ctx); +}; + +typedef struct { + /** Type of scheme */ + git_http_auth_t type; + + /** Name of the scheme (as used in the Authorization header) */ + const char *name; + + /** Credential types this scheme supports */ + git_credential_t credtypes; + + /** Function to initialize an authentication context */ + int (*init_context)( + git_http_auth_context **out, + const git_net_url *url); +} git_http_auth_scheme; + +int git_http_auth_dummy( + git_http_auth_context **out, + const git_net_url *url); + +int git_http_auth_basic( + git_http_auth_context **out, + const git_net_url *url); + +#endif diff --git a/src/libgit2/transports/auth_gssapi.c b/src/libgit2/transports/auth_gssapi.c new file mode 100644 index 0000000..5005538 --- /dev/null +++ b/src/libgit2/transports/auth_gssapi.c @@ -0,0 +1,314 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "auth_negotiate.h" + +#if defined(GIT_GSSAPI) || defined(GIT_GSSFRAMEWORK) + +#include "git2.h" +#include "auth.h" +#include "git2/sys/credential.h" + +#ifdef GIT_GSSFRAMEWORK +#import +#elif defined(GIT_GSSAPI) +#include +#include +#endif + +static gss_OID_desc gssapi_oid_spnego = + { 6, (void *) "\x2b\x06\x01\x05\x05\x02" }; +static gss_OID_desc gssapi_oid_krb5 = + { 9, (void *) "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02" }; + +static gss_OID gssapi_oids[] = + { &gssapi_oid_spnego, &gssapi_oid_krb5, NULL }; + +typedef struct { + git_http_auth_context parent; + unsigned configured : 1, + complete : 1; + git_str target; + char *challenge; + gss_ctx_id_t gss_context; + gss_OID oid; +} http_auth_gssapi_context; + +static void gssapi_err_set( + OM_uint32 status_major, + OM_uint32 status_minor, + const char *message) +{ + gss_buffer_desc buffer = GSS_C_EMPTY_BUFFER; + OM_uint32 status_display, context = 0; + + if (gss_display_status(&status_display, status_major, GSS_C_GSS_CODE, + GSS_C_NO_OID, &context, &buffer) == GSS_S_COMPLETE) { + git_error_set(GIT_ERROR_NET, "%s: %.*s (%d.%d)", + message, (int)buffer.length, (const char *)buffer.value, + status_major, status_minor); + gss_release_buffer(&status_minor, &buffer); + } else { + git_error_set(GIT_ERROR_NET, "%s: unknown negotiate error (%d.%d)", + message, status_major, status_minor); + } +} + +static int gssapi_set_challenge( + git_http_auth_context *c, + const char *challenge) +{ + http_auth_gssapi_context *ctx = (http_auth_gssapi_context *)c; + + GIT_ASSERT_ARG(ctx); + GIT_ASSERT_ARG(challenge); + GIT_ASSERT(ctx->configured); + + git__free(ctx->challenge); + + ctx->challenge = git__strdup(challenge); + GIT_ERROR_CHECK_ALLOC(ctx->challenge); + + return 0; +} + +static void gssapi_context_dispose(http_auth_gssapi_context *ctx) +{ + OM_uint32 status_minor; + + if (ctx->gss_context != GSS_C_NO_CONTEXT) { + gss_delete_sec_context( + &status_minor, &ctx->gss_context, GSS_C_NO_BUFFER); + ctx->gss_context = GSS_C_NO_CONTEXT; + } + + git_str_dispose(&ctx->target); + + git__free(ctx->challenge); + ctx->challenge = NULL; +} + +static int gssapi_next_token( + git_str *buf, + git_http_auth_context *c, + git_credential *cred) +{ + http_auth_gssapi_context *ctx = (http_auth_gssapi_context *)c; + OM_uint32 status_major, status_minor; + gss_buffer_desc target_buffer = GSS_C_EMPTY_BUFFER, + input_token = GSS_C_EMPTY_BUFFER, + output_token = GSS_C_EMPTY_BUFFER; + gss_buffer_t input_token_ptr = GSS_C_NO_BUFFER; + git_str input_buf = GIT_STR_INIT; + gss_name_t server = NULL; + gss_OID mech; + size_t challenge_len; + int error = 0; + + GIT_ASSERT_ARG(buf); + GIT_ASSERT_ARG(ctx); + GIT_ASSERT_ARG(cred); + + GIT_ASSERT(ctx->configured); + GIT_ASSERT(cred->credtype == GIT_CREDENTIAL_DEFAULT); + + if (ctx->complete) + return 0; + + target_buffer.value = (void *)ctx->target.ptr; + target_buffer.length = ctx->target.size; + + status_major = gss_import_name(&status_minor, &target_buffer, + GSS_C_NT_HOSTBASED_SERVICE, &server); + + if (GSS_ERROR(status_major)) { + gssapi_err_set(status_major, status_minor, + "could not parse principal"); + error = -1; + goto done; + } + + challenge_len = ctx->challenge ? strlen(ctx->challenge) : 0; + + if (challenge_len < 9 || memcmp(ctx->challenge, "Negotiate", 9) != 0) { + git_error_set(GIT_ERROR_NET, "server did not request negotiate"); + error = -1; + goto done; + } + + if (challenge_len > 9) { + if (git_str_decode_base64(&input_buf, + ctx->challenge + 10, challenge_len - 10) < 0) { + git_error_set(GIT_ERROR_NET, "invalid negotiate challenge from server"); + error = -1; + goto done; + } + + input_token.value = input_buf.ptr; + input_token.length = input_buf.size; + input_token_ptr = &input_token; + } else if (ctx->gss_context != GSS_C_NO_CONTEXT) { + gssapi_context_dispose(ctx); + } + + mech = &gssapi_oid_spnego; + + status_major = gss_init_sec_context( + &status_minor, + GSS_C_NO_CREDENTIAL, + &ctx->gss_context, + server, + mech, + GSS_C_DELEG_FLAG | GSS_C_MUTUAL_FLAG, + GSS_C_INDEFINITE, + GSS_C_NO_CHANNEL_BINDINGS, + input_token_ptr, + NULL, + &output_token, + NULL, + NULL); + + if (GSS_ERROR(status_major)) { + gssapi_err_set(status_major, status_minor, "negotiate failure"); + error = -1; + goto done; + } + + /* This message merely told us auth was complete; we do not respond. */ + if (status_major == GSS_S_COMPLETE) { + gssapi_context_dispose(ctx); + ctx->complete = 1; + goto done; + } + + if (output_token.length == 0) { + git_error_set(GIT_ERROR_NET, "GSSAPI did not return token"); + error = -1; + goto done; + } + + git_str_puts(buf, "Negotiate "); + git_str_encode_base64(buf, output_token.value, output_token.length); + + if (git_str_oom(buf)) + error = -1; + +done: + gss_release_name(&status_minor, &server); + gss_release_buffer(&status_minor, (gss_buffer_t) &output_token); + git_str_dispose(&input_buf); + return error; +} + +static int gssapi_is_complete(git_http_auth_context *c) +{ + http_auth_gssapi_context *ctx = (http_auth_gssapi_context *)c; + + GIT_ASSERT_ARG(ctx); + + return (ctx->complete == 1); +} + +static void gssapi_context_free(git_http_auth_context *c) +{ + http_auth_gssapi_context *ctx = (http_auth_gssapi_context *)c; + + gssapi_context_dispose(ctx); + + ctx->configured = 0; + ctx->complete = 0; + ctx->oid = NULL; + + git__free(ctx); +} + +static int gssapi_init_context( + http_auth_gssapi_context *ctx, + const git_net_url *url) +{ + OM_uint32 status_major, status_minor; + gss_OID item, *oid; + gss_OID_set mechanism_list; + size_t i; + + /* Query supported mechanisms looking for SPNEGO) */ + status_major = gss_indicate_mechs(&status_minor, &mechanism_list); + + if (GSS_ERROR(status_major)) { + gssapi_err_set(status_major, status_minor, + "could not query mechanisms"); + return -1; + } + + if (mechanism_list) { + for (oid = gssapi_oids; *oid; oid++) { + for (i = 0; i < mechanism_list->count; i++) { + item = &mechanism_list->elements[i]; + + if (item->length == (*oid)->length && + memcmp(item->elements, (*oid)->elements, item->length) == 0) { + ctx->oid = *oid; + break; + } + + } + + if (ctx->oid) + break; + } + } + + gss_release_oid_set(&status_minor, &mechanism_list); + + if (!ctx->oid) { + git_error_set(GIT_ERROR_NET, "negotiate authentication is not supported"); + return GIT_EAUTH; + } + + git_str_puts(&ctx->target, "HTTP@"); + git_str_puts(&ctx->target, url->host); + + if (git_str_oom(&ctx->target)) + return -1; + + ctx->gss_context = GSS_C_NO_CONTEXT; + ctx->configured = 1; + + return 0; +} + +int git_http_auth_negotiate( + git_http_auth_context **out, + const git_net_url *url) +{ + http_auth_gssapi_context *ctx; + + *out = NULL; + + ctx = git__calloc(1, sizeof(http_auth_gssapi_context)); + GIT_ERROR_CHECK_ALLOC(ctx); + + if (gssapi_init_context(ctx, url) < 0) { + git__free(ctx); + return -1; + } + + ctx->parent.type = GIT_HTTP_AUTH_NEGOTIATE; + ctx->parent.credtypes = GIT_CREDENTIAL_DEFAULT; + ctx->parent.connection_affinity = 1; + ctx->parent.set_challenge = gssapi_set_challenge; + ctx->parent.next_token = gssapi_next_token; + ctx->parent.is_complete = gssapi_is_complete; + ctx->parent.free = gssapi_context_free; + + *out = (git_http_auth_context *)ctx; + + return 0; +} + +#endif /* GIT_GSSAPI */ + diff --git a/src/libgit2/transports/auth_negotiate.h b/src/libgit2/transports/auth_negotiate.h new file mode 100644 index 0000000..4360785 --- /dev/null +++ b/src/libgit2/transports/auth_negotiate.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_transports_auth_negotiate_h__ +#define INCLUDE_transports_auth_negotiate_h__ + +#include "common.h" +#include "git2.h" +#include "auth.h" + +#if defined(GIT_GSSAPI) || defined(GIT_GSSFRAMEWORK) || defined(GIT_WIN32) + +extern int git_http_auth_negotiate( + git_http_auth_context **out, + const git_net_url *url); + +#else + +#define git_http_auth_negotiate git_http_auth_dummy + +#endif /* GIT_GSSAPI */ + +#endif diff --git a/src/libgit2/transports/auth_ntlm.h b/src/libgit2/transports/auth_ntlm.h new file mode 100644 index 0000000..33406ae --- /dev/null +++ b/src/libgit2/transports/auth_ntlm.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_transports_auth_ntlm_h__ +#define INCLUDE_transports_auth_ntlm_h__ + +#include "auth.h" + +/* NTLM requires a full request/challenge/response */ +#define GIT_AUTH_STEPS_NTLM 2 + +#if defined(GIT_NTLM) || defined(GIT_WIN32) + +#if defined(GIT_OPENSSL) +# define CRYPT_OPENSSL +#elif defined(GIT_MBEDTLS) +# define CRYPT_MBEDTLS +#elif defined(GIT_SECURE_TRANSPORT) +# define CRYPT_COMMONCRYPTO +#endif + +extern int git_http_auth_ntlm( + git_http_auth_context **out, + const git_net_url *url); + +#else + +#define git_http_auth_ntlm git_http_auth_dummy + +#endif /* GIT_NTLM */ + +#endif + diff --git a/src/libgit2/transports/auth_ntlmclient.c b/src/libgit2/transports/auth_ntlmclient.c new file mode 100644 index 0000000..6f26a61 --- /dev/null +++ b/src/libgit2/transports/auth_ntlmclient.c @@ -0,0 +1,227 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "auth_ntlm.h" + +#include "common.h" +#include "str.h" +#include "auth.h" +#include "git2/sys/credential.h" + +#ifdef GIT_NTLM + +#include "ntlmclient.h" + +typedef struct { + git_http_auth_context parent; + ntlm_client *ntlm; + char *challenge; + bool complete; +} http_auth_ntlm_context; + +static int ntlmclient_set_challenge( + git_http_auth_context *c, + const char *challenge) +{ + http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c; + + GIT_ASSERT_ARG(ctx); + GIT_ASSERT_ARG(challenge); + + git__free(ctx->challenge); + + ctx->challenge = git__strdup(challenge); + GIT_ERROR_CHECK_ALLOC(ctx->challenge); + + return 0; +} + +static int ntlmclient_set_credentials(http_auth_ntlm_context *ctx, git_credential *_cred) +{ + git_credential_userpass_plaintext *cred; + const char *sep, *username; + char *domain = NULL, *domainuser = NULL; + int error = 0; + + GIT_ASSERT(_cred->credtype == GIT_CREDENTIAL_USERPASS_PLAINTEXT); + cred = (git_credential_userpass_plaintext *)_cred; + + if ((sep = strchr(cred->username, '\\')) != NULL) { + domain = git__strndup(cred->username, (sep - cred->username)); + GIT_ERROR_CHECK_ALLOC(domain); + + domainuser = git__strdup(sep + 1); + GIT_ERROR_CHECK_ALLOC(domainuser); + + username = domainuser; + } else { + username = cred->username; + } + + if (ntlm_client_set_credentials(ctx->ntlm, + username, domain, cred->password) < 0) { + git_error_set(GIT_ERROR_NET, "could not set credentials: %s", + ntlm_client_errmsg(ctx->ntlm)); + error = -1; + goto done; + } + +done: + git__free(domain); + git__free(domainuser); + return error; +} + +static int ntlmclient_next_token( + git_str *buf, + git_http_auth_context *c, + git_credential *cred) +{ + http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c; + git_str input_buf = GIT_STR_INIT; + const unsigned char *msg; + size_t challenge_len, msg_len; + int error = GIT_EAUTH; + + GIT_ASSERT_ARG(buf); + GIT_ASSERT_ARG(ctx); + + GIT_ASSERT(ctx->ntlm); + + challenge_len = ctx->challenge ? strlen(ctx->challenge) : 0; + + if (ctx->complete) + ntlm_client_reset(ctx->ntlm); + + /* + * Set us complete now since it's the default case; the one + * incomplete case (successfully created a client request) + * will explicitly set that it requires a second step. + */ + ctx->complete = true; + + if (cred && ntlmclient_set_credentials(ctx, cred) != 0) + goto done; + + if (challenge_len < 4) { + git_error_set(GIT_ERROR_NET, "no ntlm challenge sent from server"); + goto done; + } else if (challenge_len == 4) { + if (memcmp(ctx->challenge, "NTLM", 4) != 0) { + git_error_set(GIT_ERROR_NET, "server did not request NTLM"); + goto done; + } + + if (ntlm_client_negotiate(&msg, &msg_len, ctx->ntlm) != 0) { + git_error_set(GIT_ERROR_NET, "ntlm authentication failed: %s", + ntlm_client_errmsg(ctx->ntlm)); + goto done; + } + + ctx->complete = false; + } else { + if (memcmp(ctx->challenge, "NTLM ", 5) != 0) { + git_error_set(GIT_ERROR_NET, "challenge from server was not NTLM"); + goto done; + } + + if (git_str_decode_base64(&input_buf, + ctx->challenge + 5, challenge_len - 5) < 0) { + git_error_set(GIT_ERROR_NET, "invalid NTLM challenge from server"); + goto done; + } + + if (ntlm_client_set_challenge(ctx->ntlm, + (const unsigned char *)input_buf.ptr, input_buf.size) != 0) { + git_error_set(GIT_ERROR_NET, "ntlm challenge failed: %s", + ntlm_client_errmsg(ctx->ntlm)); + goto done; + } + + if (ntlm_client_response(&msg, &msg_len, ctx->ntlm) != 0) { + git_error_set(GIT_ERROR_NET, "ntlm authentication failed: %s", + ntlm_client_errmsg(ctx->ntlm)); + goto done; + } + } + + git_str_puts(buf, "NTLM "); + git_str_encode_base64(buf, (const char *)msg, msg_len); + + if (git_str_oom(buf)) + goto done; + + error = 0; + +done: + git_str_dispose(&input_buf); + return error; +} + +static int ntlmclient_is_complete(git_http_auth_context *c) +{ + http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c; + + GIT_ASSERT_ARG(ctx); + return (ctx->complete == true); +} + +static void ntlmclient_context_free(git_http_auth_context *c) +{ + http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c; + + ntlm_client_free(ctx->ntlm); + git__free(ctx->challenge); + git__free(ctx); +} + +static int ntlmclient_init_context( + http_auth_ntlm_context *ctx, + const git_net_url *url) +{ + GIT_UNUSED(url); + + if ((ctx->ntlm = ntlm_client_init(NTLM_CLIENT_DEFAULTS)) == NULL) { + git_error_set_oom(); + return -1; + } + + return 0; +} + +int git_http_auth_ntlm( + git_http_auth_context **out, + const git_net_url *url) +{ + http_auth_ntlm_context *ctx; + + GIT_UNUSED(url); + + *out = NULL; + + ctx = git__calloc(1, sizeof(http_auth_ntlm_context)); + GIT_ERROR_CHECK_ALLOC(ctx); + + if (ntlmclient_init_context(ctx, url) < 0) { + git__free(ctx); + return -1; + } + + ctx->parent.type = GIT_HTTP_AUTH_NTLM; + ctx->parent.credtypes = GIT_CREDENTIAL_USERPASS_PLAINTEXT; + ctx->parent.connection_affinity = 1; + ctx->parent.set_challenge = ntlmclient_set_challenge; + ctx->parent.next_token = ntlmclient_next_token; + ctx->parent.is_complete = ntlmclient_is_complete; + ctx->parent.free = ntlmclient_context_free; + + *out = (git_http_auth_context *)ctx; + + return 0; +} + +#endif /* GIT_NTLM */ diff --git a/src/libgit2/transports/auth_sspi.c b/src/libgit2/transports/auth_sspi.c new file mode 100644 index 0000000..f826936 --- /dev/null +++ b/src/libgit2/transports/auth_sspi.c @@ -0,0 +1,341 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "auth_ntlm.h" +#include "auth_negotiate.h" + +#ifdef GIT_WIN32 + +#define SECURITY_WIN32 + +#include "git2.h" +#include "auth.h" +#include "git2/sys/credential.h" + +#include +#include + +typedef struct { + git_http_auth_context parent; + wchar_t *target; + + const char *package_name; + size_t package_name_len; + wchar_t *package_name_w; + SecPkgInfoW *package_info; + SEC_WINNT_AUTH_IDENTITY_W identity; + CredHandle cred; + CtxtHandle context; + + int has_identity : 1, + has_credentials : 1, + has_context : 1, + complete : 1; + git_str challenge; +} http_auth_sspi_context; + +static void sspi_reset_context(http_auth_sspi_context *ctx) +{ + if (ctx->has_identity) { + git__free(ctx->identity.User); + git__free(ctx->identity.Domain); + git__free(ctx->identity.Password); + + memset(&ctx->identity, 0, sizeof(SEC_WINNT_AUTH_IDENTITY_W)); + + ctx->has_identity = 0; + } + + if (ctx->has_credentials) { + FreeCredentialsHandle(&ctx->cred); + memset(&ctx->cred, 0, sizeof(CredHandle)); + + ctx->has_credentials = 0; + } + + if (ctx->has_context) { + DeleteSecurityContext(&ctx->context); + memset(&ctx->context, 0, sizeof(CtxtHandle)); + + ctx->has_context = 0; + } + + ctx->complete = 0; + + git_str_dispose(&ctx->challenge); +} + +static int sspi_set_challenge( + git_http_auth_context *c, + const char *challenge) +{ + http_auth_sspi_context *ctx = (http_auth_sspi_context *)c; + size_t challenge_len = strlen(challenge); + + git_str_clear(&ctx->challenge); + + if (strncmp(challenge, ctx->package_name, ctx->package_name_len) != 0) { + git_error_set(GIT_ERROR_NET, "invalid %s challenge from server", ctx->package_name); + return -1; + } + + /* + * A package type indicator without a base64 payload indicates the + * mechanism; it's not an actual challenge. Ignore it. + */ + if (challenge[ctx->package_name_len] == 0) { + return 0; + } else if (challenge[ctx->package_name_len] != ' ') { + git_error_set(GIT_ERROR_NET, "invalid %s challenge from server", ctx->package_name); + return -1; + } + + if (git_str_decode_base64(&ctx->challenge, + challenge + (ctx->package_name_len + 1), + challenge_len - (ctx->package_name_len + 1)) < 0) { + git_error_set(GIT_ERROR_NET, "invalid %s challenge from server", ctx->package_name); + return -1; + } + + GIT_ASSERT(ctx->challenge.size <= ULONG_MAX); + return 0; +} + +static int create_identity( + SEC_WINNT_AUTH_IDENTITY_W **out, + http_auth_sspi_context *ctx, + git_credential *cred) +{ + git_credential_userpass_plaintext *userpass; + wchar_t *username = NULL, *domain = NULL, *password = NULL; + int username_len = 0, domain_len = 0, password_len = 0; + const char *sep; + + if (cred->credtype == GIT_CREDENTIAL_DEFAULT) { + *out = NULL; + return 0; + } + + if (cred->credtype != GIT_CREDENTIAL_USERPASS_PLAINTEXT) { + git_error_set(GIT_ERROR_NET, "unknown credential type: %d", cred->credtype); + return -1; + } + + userpass = (git_credential_userpass_plaintext *)cred; + + if ((sep = strchr(userpass->username, '\\')) != NULL) { + GIT_ASSERT(sep - userpass->username < INT_MAX); + + username_len = git_utf8_to_16_alloc(&username, sep + 1); + domain_len = git_utf8_to_16_alloc_with_len(&domain, + userpass->username, (int)(sep - userpass->username)); + } else { + username_len = git_utf8_to_16_alloc(&username, + userpass->username); + } + + password_len = git_utf8_to_16_alloc(&password, userpass->password); + + if (username_len < 0 || domain_len < 0 || password_len < 0) { + git__free(username); + git__free(domain); + git__free(password); + return -1; + } + + ctx->identity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE; + ctx->identity.User = username; + ctx->identity.UserLength = (unsigned long)username_len; + ctx->identity.Password = password; + ctx->identity.PasswordLength = (unsigned long)password_len; + ctx->identity.Domain = domain; + ctx->identity.DomainLength = (unsigned long)domain_len; + + ctx->has_identity = 1; + + *out = &ctx->identity; + + return 0; +} + +static int sspi_next_token( + git_str *buf, + git_http_auth_context *c, + git_credential *cred) +{ + http_auth_sspi_context *ctx = (http_auth_sspi_context *)c; + SEC_WINNT_AUTH_IDENTITY_W *identity = NULL; + TimeStamp timestamp; + DWORD context_flags; + SecBuffer input_buf = { 0, SECBUFFER_TOKEN, NULL }; + SecBuffer output_buf = { 0, SECBUFFER_TOKEN, NULL }; + SecBufferDesc input_buf_desc = { SECBUFFER_VERSION, 1, &input_buf }; + SecBufferDesc output_buf_desc = { SECBUFFER_VERSION, 1, &output_buf }; + SECURITY_STATUS status; + + if (ctx->complete) + sspi_reset_context(ctx); + + if (!ctx->has_context) { + if (create_identity(&identity, ctx, cred) < 0) + return -1; + + status = AcquireCredentialsHandleW(NULL, ctx->package_name_w, + SECPKG_CRED_BOTH, NULL, identity, NULL, + NULL, &ctx->cred, ×tamp); + + if (status != SEC_E_OK) { + git_error_set(GIT_ERROR_OS, "could not acquire credentials"); + return -1; + } + + ctx->has_credentials = 1; + } + + context_flags = ISC_REQ_ALLOCATE_MEMORY | + ISC_REQ_CONFIDENTIALITY | + ISC_REQ_MUTUAL_AUTH; + + if (ctx->challenge.size > 0) { + input_buf.BufferType = SECBUFFER_TOKEN; + input_buf.cbBuffer = (unsigned long)ctx->challenge.size; + input_buf.pvBuffer = ctx->challenge.ptr; + } + + status = InitializeSecurityContextW(&ctx->cred, + ctx->has_context ? &ctx->context : NULL, + ctx->target, + context_flags, + 0, + SECURITY_NETWORK_DREP, + ctx->has_context ? &input_buf_desc : NULL, + 0, + ctx->has_context ? NULL : &ctx->context, + &output_buf_desc, + &context_flags, + NULL); + + if (status == SEC_I_COMPLETE_AND_CONTINUE || + status == SEC_I_COMPLETE_NEEDED) + status = CompleteAuthToken(&ctx->context, &output_buf_desc); + + if (status == SEC_E_OK) { + ctx->complete = 1; + } else if (status != SEC_I_CONTINUE_NEEDED) { + git_error_set(GIT_ERROR_OS, "could not initialize security context"); + return -1; + } + + ctx->has_context = 1; + git_str_clear(&ctx->challenge); + + if (output_buf.cbBuffer > 0) { + git_str_put(buf, ctx->package_name, ctx->package_name_len); + git_str_putc(buf, ' '); + git_str_encode_base64(buf, output_buf.pvBuffer, output_buf.cbBuffer); + + FreeContextBuffer(output_buf.pvBuffer); + + if (git_str_oom(buf)) + return -1; + } + + return 0; +} + +static int sspi_is_complete(git_http_auth_context *c) +{ + http_auth_sspi_context *ctx = (http_auth_sspi_context *)c; + + return ctx->complete; +} + +static void sspi_context_free(git_http_auth_context *c) +{ + http_auth_sspi_context *ctx = (http_auth_sspi_context *)c; + + sspi_reset_context(ctx); + + FreeContextBuffer(ctx->package_info); + git__free(ctx->target); + git__free(ctx); +} + +static int sspi_init_context( + git_http_auth_context **out, + git_http_auth_t type, + const git_net_url *url) +{ + http_auth_sspi_context *ctx; + git_str target = GIT_STR_INIT; + + *out = NULL; + + ctx = git__calloc(1, sizeof(http_auth_sspi_context)); + GIT_ERROR_CHECK_ALLOC(ctx); + + switch (type) { + case GIT_HTTP_AUTH_NTLM: + ctx->package_name = "NTLM"; + ctx->package_name_len = CONST_STRLEN("NTLM"); + ctx->package_name_w = L"NTLM"; + ctx->parent.credtypes = GIT_CREDENTIAL_USERPASS_PLAINTEXT | + GIT_CREDENTIAL_DEFAULT; + break; + case GIT_HTTP_AUTH_NEGOTIATE: + ctx->package_name = "Negotiate"; + ctx->package_name_len = CONST_STRLEN("Negotiate"); + ctx->package_name_w = L"Negotiate"; + ctx->parent.credtypes = GIT_CREDENTIAL_DEFAULT; + break; + default: + git_error_set(GIT_ERROR_NET, "unknown SSPI auth type: %d", ctx->parent.type); + git__free(ctx); + return -1; + } + + if (QuerySecurityPackageInfoW(ctx->package_name_w, &ctx->package_info) != SEC_E_OK) { + git_error_set(GIT_ERROR_OS, "could not query security package"); + git__free(ctx); + return -1; + } + + if (git_str_printf(&target, "http/%s", url->host) < 0 || + git_utf8_to_16_alloc(&ctx->target, target.ptr) < 0) { + FreeContextBuffer(ctx->package_info); + git__free(ctx); + return -1; + } + + ctx->parent.type = type; + ctx->parent.connection_affinity = 1; + ctx->parent.set_challenge = sspi_set_challenge; + ctx->parent.next_token = sspi_next_token; + ctx->parent.is_complete = sspi_is_complete; + ctx->parent.free = sspi_context_free; + + *out = (git_http_auth_context *)ctx; + + git_str_dispose(&target); + return 0; +} + +int git_http_auth_negotiate( + git_http_auth_context **out, + const git_net_url *url) +{ + return sspi_init_context(out, GIT_HTTP_AUTH_NEGOTIATE, url); +} + +int git_http_auth_ntlm( + git_http_auth_context **out, + const git_net_url *url) +{ + return sspi_init_context(out, GIT_HTTP_AUTH_NTLM, url); +} + +#endif /* GIT_WIN32 */ diff --git a/src/libgit2/transports/credential.c b/src/libgit2/transports/credential.c new file mode 100644 index 0000000..6e00b02 --- /dev/null +++ b/src/libgit2/transports/credential.c @@ -0,0 +1,486 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "git2/credential.h" +#include "git2/sys/credential.h" +#include "git2/credential_helpers.h" + +static int git_credential_ssh_key_type_new( + git_credential **cred, + const char *username, + const char *publickey, + const char *privatekey, + const char *passphrase, + git_credential_t credtype); + +int git_credential_has_username(git_credential *cred) +{ + if (cred->credtype == GIT_CREDENTIAL_DEFAULT) + return 0; + + return 1; +} + +const char *git_credential_get_username(git_credential *cred) +{ + switch (cred->credtype) { + case GIT_CREDENTIAL_USERNAME: + { + git_credential_username *c = (git_credential_username *) cred; + return c->username; + } + case GIT_CREDENTIAL_USERPASS_PLAINTEXT: + { + git_credential_userpass_plaintext *c = (git_credential_userpass_plaintext *) cred; + return c->username; + } + case GIT_CREDENTIAL_SSH_KEY: + case GIT_CREDENTIAL_SSH_MEMORY: + { + git_credential_ssh_key *c = (git_credential_ssh_key *) cred; + return c->username; + } + case GIT_CREDENTIAL_SSH_CUSTOM: + { + git_credential_ssh_custom *c = (git_credential_ssh_custom *) cred; + return c->username; + } + case GIT_CREDENTIAL_SSH_INTERACTIVE: + { + git_credential_ssh_interactive *c = (git_credential_ssh_interactive *) cred; + return c->username; + } + + default: + return NULL; + } +} + +static void plaintext_free(struct git_credential *cred) +{ + git_credential_userpass_plaintext *c = (git_credential_userpass_plaintext *)cred; + + git__free(c->username); + + /* Zero the memory which previously held the password */ + if (c->password) { + size_t pass_len = strlen(c->password); + git__memzero(c->password, pass_len); + git__free(c->password); + } + + git__free(c); +} + +int git_credential_userpass_plaintext_new( + git_credential **cred, + const char *username, + const char *password) +{ + git_credential_userpass_plaintext *c; + + GIT_ASSERT_ARG(cred); + GIT_ASSERT_ARG(username); + GIT_ASSERT_ARG(password); + + c = git__malloc(sizeof(git_credential_userpass_plaintext)); + GIT_ERROR_CHECK_ALLOC(c); + + c->parent.credtype = GIT_CREDENTIAL_USERPASS_PLAINTEXT; + c->parent.free = plaintext_free; + c->username = git__strdup(username); + + if (!c->username) { + git__free(c); + return -1; + } + + c->password = git__strdup(password); + + if (!c->password) { + git__free(c->username); + git__free(c); + return -1; + } + + *cred = &c->parent; + return 0; +} + +static void ssh_key_free(struct git_credential *cred) +{ + git_credential_ssh_key *c = + (git_credential_ssh_key *)cred; + + git__free(c->username); + + if (c->privatekey) { + /* Zero the memory which previously held the private key */ + size_t key_len = strlen(c->privatekey); + git__memzero(c->privatekey, key_len); + git__free(c->privatekey); + } + + if (c->passphrase) { + /* Zero the memory which previously held the passphrase */ + size_t pass_len = strlen(c->passphrase); + git__memzero(c->passphrase, pass_len); + git__free(c->passphrase); + } + + if (c->publickey) { + /* Zero the memory which previously held the public key */ + size_t key_len = strlen(c->publickey); + git__memzero(c->publickey, key_len); + git__free(c->publickey); + } + + git__free(c); +} + +static void ssh_interactive_free(struct git_credential *cred) +{ + git_credential_ssh_interactive *c = (git_credential_ssh_interactive *)cred; + + git__free(c->username); + + git__free(c); +} + +static void ssh_custom_free(struct git_credential *cred) +{ + git_credential_ssh_custom *c = (git_credential_ssh_custom *)cred; + + git__free(c->username); + + if (c->publickey) { + /* Zero the memory which previously held the publickey */ + size_t key_len = strlen(c->publickey); + git__memzero(c->publickey, key_len); + git__free(c->publickey); + } + + git__free(c); +} + +static void default_free(struct git_credential *cred) +{ + git_credential_default *c = (git_credential_default *)cred; + + git__free(c); +} + +static void username_free(struct git_credential *cred) +{ + git__free(cred); +} + +int git_credential_ssh_key_new( + git_credential **cred, + const char *username, + const char *publickey, + const char *privatekey, + const char *passphrase) +{ + return git_credential_ssh_key_type_new( + cred, + username, + publickey, + privatekey, + passphrase, + GIT_CREDENTIAL_SSH_KEY); +} + +int git_credential_ssh_key_memory_new( + git_credential **cred, + const char *username, + const char *publickey, + const char *privatekey, + const char *passphrase) +{ +#ifdef GIT_SSH_MEMORY_CREDENTIALS + return git_credential_ssh_key_type_new( + cred, + username, + publickey, + privatekey, + passphrase, + GIT_CREDENTIAL_SSH_MEMORY); +#else + GIT_UNUSED(cred); + GIT_UNUSED(username); + GIT_UNUSED(publickey); + GIT_UNUSED(privatekey); + GIT_UNUSED(passphrase); + + git_error_set(GIT_ERROR_INVALID, + "this version of libgit2 was not built with ssh memory credentials."); + return -1; +#endif +} + +static int git_credential_ssh_key_type_new( + git_credential **cred, + const char *username, + const char *publickey, + const char *privatekey, + const char *passphrase, + git_credential_t credtype) +{ + git_credential_ssh_key *c; + + GIT_ASSERT_ARG(username); + GIT_ASSERT_ARG(cred); + GIT_ASSERT_ARG(privatekey); + + c = git__calloc(1, sizeof(git_credential_ssh_key)); + GIT_ERROR_CHECK_ALLOC(c); + + c->parent.credtype = credtype; + c->parent.free = ssh_key_free; + + c->username = git__strdup(username); + GIT_ERROR_CHECK_ALLOC(c->username); + + c->privatekey = git__strdup(privatekey); + GIT_ERROR_CHECK_ALLOC(c->privatekey); + + if (publickey) { + c->publickey = git__strdup(publickey); + GIT_ERROR_CHECK_ALLOC(c->publickey); + } + + if (passphrase) { + c->passphrase = git__strdup(passphrase); + GIT_ERROR_CHECK_ALLOC(c->passphrase); + } + + *cred = &c->parent; + return 0; +} + +int git_credential_ssh_interactive_new( + git_credential **out, + const char *username, + git_credential_ssh_interactive_cb prompt_callback, + void *payload) +{ + git_credential_ssh_interactive *c; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(username); + GIT_ASSERT_ARG(prompt_callback); + + c = git__calloc(1, sizeof(git_credential_ssh_interactive)); + GIT_ERROR_CHECK_ALLOC(c); + + c->parent.credtype = GIT_CREDENTIAL_SSH_INTERACTIVE; + c->parent.free = ssh_interactive_free; + + c->username = git__strdup(username); + GIT_ERROR_CHECK_ALLOC(c->username); + + c->prompt_callback = prompt_callback; + c->payload = payload; + + *out = &c->parent; + return 0; +} + +int git_credential_ssh_key_from_agent(git_credential **cred, const char *username) { + git_credential_ssh_key *c; + + GIT_ASSERT_ARG(username); + GIT_ASSERT_ARG(cred); + + c = git__calloc(1, sizeof(git_credential_ssh_key)); + GIT_ERROR_CHECK_ALLOC(c); + + c->parent.credtype = GIT_CREDENTIAL_SSH_KEY; + c->parent.free = ssh_key_free; + + c->username = git__strdup(username); + GIT_ERROR_CHECK_ALLOC(c->username); + + c->privatekey = NULL; + + *cred = &c->parent; + return 0; +} + +int git_credential_ssh_custom_new( + git_credential **cred, + const char *username, + const char *publickey, + size_t publickey_len, + git_credential_sign_cb sign_callback, + void *payload) +{ + git_credential_ssh_custom *c; + + GIT_ASSERT_ARG(username); + GIT_ASSERT_ARG(cred); + + c = git__calloc(1, sizeof(git_credential_ssh_custom)); + GIT_ERROR_CHECK_ALLOC(c); + + c->parent.credtype = GIT_CREDENTIAL_SSH_CUSTOM; + c->parent.free = ssh_custom_free; + + c->username = git__strdup(username); + GIT_ERROR_CHECK_ALLOC(c->username); + + if (publickey_len > 0) { + c->publickey = git__malloc(publickey_len); + GIT_ERROR_CHECK_ALLOC(c->publickey); + + memcpy(c->publickey, publickey, publickey_len); + } + + c->publickey_len = publickey_len; + c->sign_callback = sign_callback; + c->payload = payload; + + *cred = &c->parent; + return 0; +} + +int git_credential_default_new(git_credential **cred) +{ + git_credential_default *c; + + GIT_ASSERT_ARG(cred); + + c = git__calloc(1, sizeof(git_credential_default)); + GIT_ERROR_CHECK_ALLOC(c); + + c->credtype = GIT_CREDENTIAL_DEFAULT; + c->free = default_free; + + *cred = c; + return 0; +} + +int git_credential_username_new(git_credential **cred, const char *username) +{ + git_credential_username *c; + size_t len, allocsize; + + GIT_ASSERT_ARG(cred); + + len = strlen(username); + + GIT_ERROR_CHECK_ALLOC_ADD(&allocsize, sizeof(git_credential_username), len); + GIT_ERROR_CHECK_ALLOC_ADD(&allocsize, allocsize, 1); + c = git__malloc(allocsize); + GIT_ERROR_CHECK_ALLOC(c); + + c->parent.credtype = GIT_CREDENTIAL_USERNAME; + c->parent.free = username_free; + memcpy(c->username, username, len + 1); + + *cred = (git_credential *) c; + return 0; +} + +void git_credential_free(git_credential *cred) +{ + if (!cred) + return; + + cred->free(cred); +} + +/* Deprecated credential functions */ + +#ifndef GIT_DEPRECATE_HARD +int git_cred_has_username(git_credential *cred) +{ + return git_credential_has_username(cred); +} + +const char *git_cred_get_username(git_credential *cred) +{ + return git_credential_get_username(cred); +} + +int git_cred_userpass_plaintext_new( + git_credential **out, + const char *username, + const char *password) +{ + return git_credential_userpass_plaintext_new(out,username, password); +} + +int git_cred_default_new(git_credential **out) +{ + return git_credential_default_new(out); +} + +int git_cred_username_new(git_credential **out, const char *username) +{ + return git_credential_username_new(out, username); +} + +int git_cred_ssh_key_new( + git_credential **out, + const char *username, + const char *publickey, + const char *privatekey, + const char *passphrase) +{ + return git_credential_ssh_key_new(out, username, + publickey, privatekey, passphrase); +} + +int git_cred_ssh_key_memory_new( + git_credential **out, + const char *username, + const char *publickey, + const char *privatekey, + const char *passphrase) +{ + return git_credential_ssh_key_memory_new(out, username, + publickey, privatekey, passphrase); +} + +int git_cred_ssh_interactive_new( + git_credential **out, + const char *username, + git_credential_ssh_interactive_cb prompt_callback, + void *payload) +{ + return git_credential_ssh_interactive_new(out, username, + prompt_callback, payload); +} + +int git_cred_ssh_key_from_agent( + git_credential **out, + const char *username) +{ + return git_credential_ssh_key_from_agent(out, username); +} + +int git_cred_ssh_custom_new( + git_credential **out, + const char *username, + const char *publickey, + size_t publickey_len, + git_credential_sign_cb sign_callback, + void *payload) +{ + return git_credential_ssh_custom_new(out, username, + publickey, publickey_len, sign_callback, payload); +} + +void git_cred_free(git_credential *cred) +{ + git_credential_free(cred); +} +#endif diff --git a/src/libgit2/transports/credential_helpers.c b/src/libgit2/transports/credential_helpers.c new file mode 100644 index 0000000..6d34a4e --- /dev/null +++ b/src/libgit2/transports/credential_helpers.c @@ -0,0 +1,68 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "git2/credential_helpers.h" + +int git_credential_userpass( + git_credential **cred, + const char *url, + const char *user_from_url, + unsigned int allowed_types, + void *payload) +{ + git_credential_userpass_payload *userpass = (git_credential_userpass_payload*)payload; + const char *effective_username = NULL; + + GIT_UNUSED(url); + + if (!userpass || !userpass->password) return -1; + + /* Username resolution: a username can be passed with the URL, the + * credentials payload, or both. Here's what we do. Note that if we get + * this far, we know that any password the url may contain has already + * failed at least once, so we ignore it. + * + * | Payload | URL | Used | + * +-------------+----------+-----------+ + * | yes | no | payload | + * | yes | yes | payload | + * | no | yes | url | + * | no | no | FAIL | + */ + if (userpass->username) + effective_username = userpass->username; + else if (user_from_url) + effective_username = user_from_url; + else + return -1; + + if (GIT_CREDENTIAL_USERNAME & allowed_types) + return git_credential_username_new(cred, effective_username); + + if ((GIT_CREDENTIAL_USERPASS_PLAINTEXT & allowed_types) == 0 || + git_credential_userpass_plaintext_new(cred, effective_username, userpass->password) < 0) + return -1; + + return 0; +} + +/* Deprecated credential functions */ + +#ifndef GIT_DEPRECATE_HARD +int git_cred_userpass( + git_credential **out, + const char *url, + const char *user_from_url, + unsigned int allowed_types, + void *payload) +{ + return git_credential_userpass(out, url, user_from_url, + allowed_types, payload); +} +#endif diff --git a/src/libgit2/transports/git.c b/src/libgit2/transports/git.c new file mode 100644 index 0000000..53611f2 --- /dev/null +++ b/src/libgit2/transports/git.c @@ -0,0 +1,360 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "net.h" +#include "stream.h" +#include "streams/socket.h" +#include "git2/sys/transport.h" + +#define OWNING_SUBTRANSPORT(s) ((git_subtransport *)(s)->parent.subtransport) + +static const char prefix_git[] = "git://"; +static const char cmd_uploadpack[] = "git-upload-pack"; +static const char cmd_receivepack[] = "git-receive-pack"; + +typedef struct { + git_smart_subtransport_stream parent; + git_stream *io; + const char *cmd; + char *url; + unsigned sent_command : 1; +} git_proto_stream; + +typedef struct { + git_smart_subtransport parent; + git_transport *owner; + git_proto_stream *current_stream; +} git_subtransport; + +/* + * Create a git protocol request. + * + * For example: 0035git-upload-pack /libgit2/libgit2\0host=github.com\0 + */ +static int gen_proto(git_str *request, const char *cmd, const char *url) +{ + char *delim, *repo; + char host[] = "host="; + size_t len; + + delim = strchr(url, '/'); + if (delim == NULL) { + git_error_set(GIT_ERROR_NET, "malformed URL"); + return -1; + } + + repo = delim; + if (repo[1] == '~') + ++repo; + + delim = strchr(url, ':'); + if (delim == NULL) + delim = strchr(url, '/'); + + len = 4 + strlen(cmd) + 1 + strlen(repo) + 1 + strlen(host) + (delim - url) + 1; + + git_str_grow(request, len); + git_str_printf(request, "%04x%s %s%c%s", + (unsigned int)(len & 0x0FFFF), cmd, repo, 0, host); + git_str_put(request, url, delim - url); + git_str_putc(request, '\0'); + + if (git_str_oom(request)) + return -1; + + return 0; +} + +static int send_command(git_proto_stream *s) +{ + git_str request = GIT_STR_INIT; + int error; + + if ((error = gen_proto(&request, s->cmd, s->url)) < 0) + goto cleanup; + + if ((error = git_stream__write_full(s->io, request.ptr, request.size, 0)) < 0) + goto cleanup; + + s->sent_command = 1; + +cleanup: + git_str_dispose(&request); + return error; +} + +static int git_proto_stream_read( + git_smart_subtransport_stream *stream, + char *buffer, + size_t buf_size, + size_t *bytes_read) +{ + git_proto_stream *s = (git_proto_stream *)stream; + ssize_t ret; + int error; + + *bytes_read = 0; + + if (!s->sent_command && (error = send_command(s)) < 0) + return error; + + ret = git_stream_read(s->io, buffer, min(buf_size, INT_MAX)); + + if (ret < 0) + return -1; + + *bytes_read = (size_t)ret; + return 0; +} + +static int git_proto_stream_write( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + git_proto_stream *s = (git_proto_stream *)stream; + int error; + + if (!s->sent_command && (error = send_command(s)) < 0) + return error; + + return git_stream__write_full(s->io, buffer, len, 0); +} + +static void git_proto_stream_free(git_smart_subtransport_stream *stream) +{ + git_proto_stream *s; + git_subtransport *t; + + if (!stream) + return; + + s = (git_proto_stream *)stream; + t = OWNING_SUBTRANSPORT(s); + + t->current_stream = NULL; + + git_stream_close(s->io); + git_stream_free(s->io); + git__free(s->url); + git__free(s); +} + +static int git_proto_stream_alloc( + git_subtransport *t, + const char *url, + const char *cmd, + const char *host, + const char *port, + git_smart_subtransport_stream **stream) +{ + git_proto_stream *s; + + if (!stream) + return -1; + + s = git__calloc(1, sizeof(git_proto_stream)); + GIT_ERROR_CHECK_ALLOC(s); + + s->parent.subtransport = &t->parent; + s->parent.read = git_proto_stream_read; + s->parent.write = git_proto_stream_write; + s->parent.free = git_proto_stream_free; + + s->cmd = cmd; + s->url = git__strdup(url); + + if (!s->url) { + git__free(s); + return -1; + } + + if ((git_socket_stream_new(&s->io, host, port)) < 0) + return -1; + + GIT_ERROR_CHECK_VERSION(s->io, GIT_STREAM_VERSION, "git_stream"); + + *stream = &s->parent; + return 0; +} + +static int _git_uploadpack_ls( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + git_net_url urldata = GIT_NET_URL_INIT; + const char *stream_url = url; + const char *host, *port; + git_proto_stream *s; + int error; + + *stream = NULL; + + if (!git__prefixcmp(url, prefix_git)) + stream_url += strlen(prefix_git); + + if ((error = git_net_url_parse(&urldata, url)) < 0) + return error; + + host = urldata.host; + port = urldata.port ? urldata.port : GIT_DEFAULT_PORT; + + error = git_proto_stream_alloc(t, stream_url, cmd_uploadpack, host, port, stream); + + git_net_url_dispose(&urldata); + + if (error < 0) { + git_proto_stream_free(*stream); + return error; + } + + s = (git_proto_stream *) *stream; + if ((error = git_stream_connect(s->io)) < 0) { + git_proto_stream_free(*stream); + return error; + } + + t->current_stream = s; + + return 0; +} + +static int _git_uploadpack( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + GIT_UNUSED(url); + + if (t->current_stream) { + *stream = &t->current_stream->parent; + return 0; + } + + git_error_set(GIT_ERROR_NET, "must call UPLOADPACK_LS before UPLOADPACK"); + return -1; +} + +static int _git_receivepack_ls( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + git_net_url urldata = GIT_NET_URL_INIT; + const char *stream_url = url; + git_proto_stream *s; + int error; + + *stream = NULL; + if (!git__prefixcmp(url, prefix_git)) + stream_url += strlen(prefix_git); + + if ((error = git_net_url_parse(&urldata, url)) < 0) + return error; + + error = git_proto_stream_alloc(t, stream_url, cmd_receivepack, urldata.host, urldata.port, stream); + + git_net_url_dispose(&urldata); + + if (error < 0) { + git_proto_stream_free(*stream); + return error; + } + + s = (git_proto_stream *) *stream; + + if ((error = git_stream_connect(s->io)) < 0) + return error; + + t->current_stream = s; + + return 0; +} + +static int _git_receivepack( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + GIT_UNUSED(url); + + if (t->current_stream) { + *stream = &t->current_stream->parent; + return 0; + } + + git_error_set(GIT_ERROR_NET, "must call RECEIVEPACK_LS before RECEIVEPACK"); + return -1; +} + +static int _git_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) +{ + git_subtransport *t = (git_subtransport *) subtransport; + + switch (action) { + case GIT_SERVICE_UPLOADPACK_LS: + return _git_uploadpack_ls(t, url, stream); + + case GIT_SERVICE_UPLOADPACK: + return _git_uploadpack(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK_LS: + return _git_receivepack_ls(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK: + return _git_receivepack(t, url, stream); + } + + *stream = NULL; + return -1; +} + +static int _git_close(git_smart_subtransport *subtransport) +{ + git_subtransport *t = (git_subtransport *) subtransport; + + GIT_ASSERT(!t->current_stream); + + GIT_UNUSED(t); + + return 0; +} + +static void _git_free(git_smart_subtransport *subtransport) +{ + git_subtransport *t = (git_subtransport *) subtransport; + + git__free(t); +} + +int git_smart_subtransport_git(git_smart_subtransport **out, git_transport *owner, void *param) +{ + git_subtransport *t; + + GIT_UNUSED(param); + + if (!out) + return -1; + + t = git__calloc(1, sizeof(git_subtransport)); + GIT_ERROR_CHECK_ALLOC(t); + + t->owner = owner; + t->parent.action = _git_action; + t->parent.close = _git_close; + t->parent.free = _git_free; + + *out = (git_smart_subtransport *) t; + return 0; +} diff --git a/src/libgit2/transports/http.c b/src/libgit2/transports/http.c new file mode 100644 index 0000000..8437674 --- /dev/null +++ b/src/libgit2/transports/http.c @@ -0,0 +1,766 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#ifndef GIT_WINHTTP + +#include "http_parser.h" +#include "net.h" +#include "remote.h" +#include "smart.h" +#include "auth.h" +#include "http.h" +#include "auth_negotiate.h" +#include "auth_ntlm.h" +#include "trace.h" +#include "streams/tls.h" +#include "streams/socket.h" +#include "httpclient.h" +#include "git2/sys/credential.h" + +bool git_http__expect_continue = false; + +typedef enum { + HTTP_STATE_NONE = 0, + HTTP_STATE_SENDING_REQUEST, + HTTP_STATE_RECEIVING_RESPONSE, + HTTP_STATE_DONE +} http_state; + +typedef struct { + git_http_method method; + const char *url; + const char *request_type; + const char *response_type; + unsigned int initial : 1, + chunked : 1; +} http_service; + +typedef struct { + git_smart_subtransport_stream parent; + const http_service *service; + http_state state; + unsigned replay_count; +} http_stream; + +typedef struct { + git_net_url url; + + git_credential *cred; + unsigned auth_schemetypes; + unsigned url_cred_presented : 1; +} http_server; + +typedef struct { + git_smart_subtransport parent; + transport_smart *owner; + + http_server server; + http_server proxy; + + git_http_client *http_client; +} http_subtransport; + +static const http_service upload_pack_ls_service = { + GIT_HTTP_METHOD_GET, "/info/refs?service=git-upload-pack", + NULL, + "application/x-git-upload-pack-advertisement", + 1, + 0 +}; +static const http_service upload_pack_service = { + GIT_HTTP_METHOD_POST, "/git-upload-pack", + "application/x-git-upload-pack-request", + "application/x-git-upload-pack-result", + 0, + 0 +}; +static const http_service receive_pack_ls_service = { + GIT_HTTP_METHOD_GET, "/info/refs?service=git-receive-pack", + NULL, + "application/x-git-receive-pack-advertisement", + 1, + 0 +}; +static const http_service receive_pack_service = { + GIT_HTTP_METHOD_POST, "/git-receive-pack", + "application/x-git-receive-pack-request", + "application/x-git-receive-pack-result", + 0, + 1 +}; + +#define SERVER_TYPE_REMOTE "remote" +#define SERVER_TYPE_PROXY "proxy" + +#define OWNING_SUBTRANSPORT(s) ((http_subtransport *)(s)->parent.subtransport) + +static int apply_url_credentials( + git_credential **cred, + unsigned int allowed_types, + const char *username, + const char *password) +{ + GIT_ASSERT_ARG(username); + + if (!password) + password = ""; + + if (allowed_types & GIT_CREDENTIAL_USERPASS_PLAINTEXT) + return git_credential_userpass_plaintext_new(cred, username, password); + + if ((allowed_types & GIT_CREDENTIAL_DEFAULT) && *username == '\0' && *password == '\0') + return git_credential_default_new(cred); + + return GIT_PASSTHROUGH; +} + +GIT_INLINE(void) free_cred(git_credential **cred) +{ + if (*cred) { + git_credential_free(*cred); + (*cred) = NULL; + } +} + +static int handle_auth( + http_server *server, + const char *server_type, + const char *url, + unsigned int allowed_schemetypes, + unsigned int allowed_credtypes, + git_credential_acquire_cb callback, + void *callback_payload) +{ + int error = 1; + + if (server->cred) + free_cred(&server->cred); + + /* Start with URL-specified credentials, if there were any. */ + if ((allowed_credtypes & GIT_CREDENTIAL_USERPASS_PLAINTEXT) && + !server->url_cred_presented && + server->url.username) { + error = apply_url_credentials(&server->cred, allowed_credtypes, server->url.username, server->url.password); + server->url_cred_presented = 1; + + /* treat GIT_PASSTHROUGH as if callback isn't set */ + if (error == GIT_PASSTHROUGH) + error = 1; + } + + if (error > 0 && callback) { + error = callback(&server->cred, url, server->url.username, allowed_credtypes, callback_payload); + + /* treat GIT_PASSTHROUGH as if callback isn't set */ + if (error == GIT_PASSTHROUGH) + error = 1; + } + + if (error > 0) { + git_error_set(GIT_ERROR_HTTP, "%s authentication required but no callback set", server_type); + error = GIT_EAUTH; + } + + if (!error) + server->auth_schemetypes = allowed_schemetypes; + + return error; +} + +GIT_INLINE(int) handle_remote_auth( + http_stream *stream, + git_http_response *response) +{ + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + git_remote_connect_options *connect_opts = &transport->owner->connect_opts; + + if (response->server_auth_credtypes == 0) { + git_error_set(GIT_ERROR_HTTP, "server requires authentication that we do not support"); + return GIT_EAUTH; + } + + /* Otherwise, prompt for credentials. */ + return handle_auth( + &transport->server, + SERVER_TYPE_REMOTE, + transport->owner->url, + response->server_auth_schemetypes, + response->server_auth_credtypes, + connect_opts->callbacks.credentials, + connect_opts->callbacks.payload); +} + +GIT_INLINE(int) handle_proxy_auth( + http_stream *stream, + git_http_response *response) +{ + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + git_remote_connect_options *connect_opts = &transport->owner->connect_opts; + + if (response->proxy_auth_credtypes == 0) { + git_error_set(GIT_ERROR_HTTP, "proxy requires authentication that we do not support"); + return GIT_EAUTH; + } + + /* Otherwise, prompt for credentials. */ + return handle_auth( + &transport->proxy, + SERVER_TYPE_PROXY, + connect_opts->proxy_opts.url, + response->server_auth_schemetypes, + response->proxy_auth_credtypes, + connect_opts->proxy_opts.credentials, + connect_opts->proxy_opts.payload); +} + +static bool allow_redirect(http_stream *stream) +{ + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + + switch (transport->owner->connect_opts.follow_redirects) { + case GIT_REMOTE_REDIRECT_INITIAL: + return (stream->service->initial == 1); + case GIT_REMOTE_REDIRECT_ALL: + return true; + default: + return false; + } +} + +static int handle_response( + bool *complete, + http_stream *stream, + git_http_response *response, + bool allow_replay) +{ + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + int error; + + *complete = false; + + if (allow_replay && git_http_response_is_redirect(response)) { + if (!response->location) { + git_error_set(GIT_ERROR_HTTP, "redirect without location"); + return -1; + } + + if (git_net_url_apply_redirect(&transport->server.url, response->location, allow_redirect(stream), stream->service->url) < 0) { + return -1; + } + + return 0; + } else if (git_http_response_is_redirect(response)) { + git_error_set(GIT_ERROR_HTTP, "unexpected redirect"); + return -1; + } + + /* If we're in the middle of challenge/response auth, continue. */ + if (allow_replay && response->resend_credentials) { + return 0; + } else if (allow_replay && response->status == GIT_HTTP_STATUS_UNAUTHORIZED) { + if ((error = handle_remote_auth(stream, response)) < 0) + return error; + + return git_http_client_skip_body(transport->http_client); + } else if (allow_replay && response->status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED) { + if ((error = handle_proxy_auth(stream, response)) < 0) + return error; + + return git_http_client_skip_body(transport->http_client); + } else if (response->status == GIT_HTTP_STATUS_UNAUTHORIZED || + response->status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED) { + git_error_set(GIT_ERROR_HTTP, "unexpected authentication failure"); + return GIT_EAUTH; + } + + if (response->status != GIT_HTTP_STATUS_OK) { + git_error_set(GIT_ERROR_HTTP, "unexpected http status code: %d", response->status); + return -1; + } + + /* The response must contain a Content-Type header. */ + if (!response->content_type) { + git_error_set(GIT_ERROR_HTTP, "no content-type header in response"); + return -1; + } + + /* The Content-Type header must match our expectation. */ + if (strcmp(response->content_type, stream->service->response_type) != 0) { + git_error_set(GIT_ERROR_HTTP, "invalid content-type: '%s'", response->content_type); + return -1; + } + + *complete = true; + stream->state = HTTP_STATE_RECEIVING_RESPONSE; + return 0; +} + +static int lookup_proxy( + bool *out_use, + http_subtransport *transport) +{ + git_remote_connect_options *connect_opts = &transport->owner->connect_opts; + const char *proxy; + git_remote *remote; + char *config = NULL; + int error = 0; + + *out_use = false; + git_net_url_dispose(&transport->proxy.url); + + switch (connect_opts->proxy_opts.type) { + case GIT_PROXY_SPECIFIED: + proxy = connect_opts->proxy_opts.url; + break; + + case GIT_PROXY_AUTO: + remote = transport->owner->owner; + + error = git_remote__http_proxy(&config, remote, &transport->server.url); + + if (error || !config) + goto done; + + proxy = config; + break; + + default: + return 0; + } + + if (!proxy || + (error = git_net_url_parse_http(&transport->proxy.url, proxy)) < 0) + goto done; + + if (!git_net_url_valid(&transport->proxy.url)) { + git_error_set(GIT_ERROR_HTTP, "invalid URL: '%s'", proxy); + error = -1; + goto done; + } + + *out_use = true; + +done: + git__free(config); + return error; +} + +static int generate_request( + git_net_url *url, + git_http_request *request, + http_stream *stream, + size_t len) +{ + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + bool use_proxy = false; + int error; + + if ((error = git_net_url_joinpath(url, + &transport->server.url, stream->service->url)) < 0 || + (error = lookup_proxy(&use_proxy, transport)) < 0) + return error; + + request->method = stream->service->method; + request->url = url; + request->credentials = transport->server.cred; + request->proxy = use_proxy ? &transport->proxy.url : NULL; + request->proxy_credentials = transport->proxy.cred; + request->custom_headers = &transport->owner->connect_opts.custom_headers; + + if (stream->service->method == GIT_HTTP_METHOD_POST) { + request->chunked = stream->service->chunked; + request->content_length = stream->service->chunked ? 0 : len; + request->content_type = stream->service->request_type; + request->accept = stream->service->response_type; + request->expect_continue = git_http__expect_continue; + } + + return 0; +} + +/* + * Read from an HTTP transport - for the first invocation of this function + * (ie, when stream->state == HTTP_STATE_NONE), we'll send a GET request + * to the remote host. We will stream that data back on all subsequent + * calls. + */ +static int http_stream_read( + git_smart_subtransport_stream *s, + char *buffer, + size_t buffer_size, + size_t *out_len) +{ + http_stream *stream = (http_stream *)s; + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + git_net_url url = GIT_NET_URL_INIT; + git_net_url proxy_url = GIT_NET_URL_INIT; + git_http_request request = {0}; + git_http_response response = {0}; + bool complete; + int error; + + *out_len = 0; + + if (stream->state == HTTP_STATE_NONE) { + stream->state = HTTP_STATE_SENDING_REQUEST; + stream->replay_count = 0; + } + + /* + * Formulate the URL, send the request and read the response + * headers. Some of the request body may also be read. + */ + while (stream->state == HTTP_STATE_SENDING_REQUEST && + stream->replay_count < GIT_HTTP_REPLAY_MAX) { + git_net_url_dispose(&url); + git_net_url_dispose(&proxy_url); + git_http_response_dispose(&response); + + if ((error = generate_request(&url, &request, stream, 0)) < 0 || + (error = git_http_client_send_request( + transport->http_client, &request)) < 0 || + (error = git_http_client_read_response( + &response, transport->http_client)) < 0 || + (error = handle_response(&complete, stream, &response, true)) < 0) + goto done; + + if (complete) + break; + + stream->replay_count++; + } + + if (stream->state == HTTP_STATE_SENDING_REQUEST) { + git_error_set(GIT_ERROR_HTTP, "too many redirects or authentication replays"); + error = GIT_ERROR; /* not GIT_EAUTH, because the exact cause is unclear */ + goto done; + } + + GIT_ASSERT(stream->state == HTTP_STATE_RECEIVING_RESPONSE); + + error = git_http_client_read_body(transport->http_client, buffer, buffer_size); + + if (error > 0) { + *out_len = error; + error = 0; + } + +done: + git_net_url_dispose(&url); + git_net_url_dispose(&proxy_url); + git_http_response_dispose(&response); + + return error; +} + +static bool needs_probe(http_stream *stream) +{ + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + + return (transport->server.auth_schemetypes == GIT_HTTP_AUTH_NTLM || + transport->server.auth_schemetypes == GIT_HTTP_AUTH_NEGOTIATE); +} + +static int send_probe(http_stream *stream) +{ + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + git_http_client *client = transport->http_client; + const char *probe = "0000"; + size_t len = 4; + git_net_url url = GIT_NET_URL_INIT; + git_http_request request = {0}; + git_http_response response = {0}; + bool complete = false; + size_t step, steps = 1; + int error; + + /* NTLM requires a full challenge/response */ + if (transport->server.auth_schemetypes == GIT_HTTP_AUTH_NTLM) + steps = GIT_AUTH_STEPS_NTLM; + + /* + * Send at most two requests: one without any authentication to see + * if we get prompted to authenticate. If we do, send a second one + * with the first authentication message. The final authentication + * message with the response will occur with the *actual* POST data. + */ + for (step = 0; step < steps && !complete; step++) { + git_net_url_dispose(&url); + git_http_response_dispose(&response); + + if ((error = generate_request(&url, &request, stream, len)) < 0 || + (error = git_http_client_send_request(client, &request)) < 0 || + (error = git_http_client_send_body(client, probe, len)) < 0 || + (error = git_http_client_read_response(&response, client)) < 0 || + (error = git_http_client_skip_body(client)) < 0 || + (error = handle_response(&complete, stream, &response, true)) < 0) + goto done; + } + +done: + git_http_response_dispose(&response); + git_net_url_dispose(&url); + return error; +} + +/* +* Write to an HTTP transport - for the first invocation of this function +* (ie, when stream->state == HTTP_STATE_NONE), we'll send a POST request +* to the remote host. If we're sending chunked data, then subsequent calls +* will write the additional data given in the buffer. If we're not chunking, +* then the caller should have given us all the data in the original call. +* The caller should call http_stream_read_response to get the result. +*/ +static int http_stream_write( + git_smart_subtransport_stream *s, + const char *buffer, + size_t len) +{ + http_stream *stream = GIT_CONTAINER_OF(s, http_stream, parent); + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + git_net_url url = GIT_NET_URL_INIT; + git_http_request request = {0}; + git_http_response response = {0}; + int error; + + while (stream->state == HTTP_STATE_NONE && + stream->replay_count < GIT_HTTP_REPLAY_MAX) { + + git_net_url_dispose(&url); + git_http_response_dispose(&response); + + /* + * If we're authenticating with a connection-based mechanism + * (NTLM, Kerberos), send a "probe" packet. Servers SHOULD + * authenticate an entire keep-alive connection, so ideally + * we should not need to authenticate but some servers do + * not support this. By sending a probe packet, we'll be + * able to follow up with a second POST using the actual + * data (and, in the degenerate case, the authentication + * header as well). + */ + if (needs_probe(stream) && (error = send_probe(stream)) < 0) + goto done; + + /* Send the regular POST request. */ + if ((error = generate_request(&url, &request, stream, len)) < 0 || + (error = git_http_client_send_request( + transport->http_client, &request)) < 0) + goto done; + + if (request.expect_continue && + git_http_client_has_response(transport->http_client)) { + bool complete; + + /* + * If we got a response to an expect/continue, then + * it's something other than a 100 and we should + * deal with the response somehow. + */ + if ((error = git_http_client_read_response(&response, transport->http_client)) < 0 || + (error = handle_response(&complete, stream, &response, true)) < 0) + goto done; + } else { + stream->state = HTTP_STATE_SENDING_REQUEST; + } + + stream->replay_count++; + } + + if (stream->state == HTTP_STATE_NONE) { + git_error_set(GIT_ERROR_HTTP, + "too many redirects or authentication replays"); + error = GIT_ERROR; /* not GIT_EAUTH because the exact cause is unclear */ + goto done; + } + + GIT_ASSERT(stream->state == HTTP_STATE_SENDING_REQUEST); + + error = git_http_client_send_body(transport->http_client, buffer, len); + +done: + git_http_response_dispose(&response); + git_net_url_dispose(&url); + return error; +} + +/* +* Read from an HTTP transport after it has been written to. This is the +* response from a POST request made by http_stream_write. +*/ +static int http_stream_read_response( + git_smart_subtransport_stream *s, + char *buffer, + size_t buffer_size, + size_t *out_len) +{ + http_stream *stream = (http_stream *)s; + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + git_http_client *client = transport->http_client; + git_http_response response = {0}; + bool complete; + int error; + + *out_len = 0; + + if (stream->state == HTTP_STATE_SENDING_REQUEST) { + if ((error = git_http_client_read_response(&response, client)) < 0 || + (error = handle_response(&complete, stream, &response, false)) < 0) + goto done; + + GIT_ASSERT(complete); + stream->state = HTTP_STATE_RECEIVING_RESPONSE; + } + + error = git_http_client_read_body(client, buffer, buffer_size); + + if (error > 0) { + *out_len = error; + error = 0; + } + +done: + git_http_response_dispose(&response); + return error; +} + +static void http_stream_free(git_smart_subtransport_stream *stream) +{ + http_stream *s = GIT_CONTAINER_OF(stream, http_stream, parent); + git__free(s); +} + +static const http_service *select_service(git_smart_service_t action) +{ + switch (action) { + case GIT_SERVICE_UPLOADPACK_LS: + return &upload_pack_ls_service; + case GIT_SERVICE_UPLOADPACK: + return &upload_pack_service; + case GIT_SERVICE_RECEIVEPACK_LS: + return &receive_pack_ls_service; + case GIT_SERVICE_RECEIVEPACK: + return &receive_pack_service; + } + + return NULL; +} + +static int http_action( + git_smart_subtransport_stream **out, + git_smart_subtransport *t, + const char *url, + git_smart_service_t action) +{ + http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent); + git_remote_connect_options *connect_opts = &transport->owner->connect_opts; + git_http_client_options opts = {0}; + http_stream *stream; + const http_service *service; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(t); + + *out = NULL; + + /* + * If we've seen a redirect then preserve the location that we've + * been given. This is important to continue authorization against + * the redirect target, not the user-given source; the endpoint may + * have redirected us from HTTP->HTTPS and is using an auth mechanism + * that would be insecure in plaintext (eg, HTTP Basic). + */ + if (!git_net_url_valid(&transport->server.url) && + (error = git_net_url_parse(&transport->server.url, url)) < 0) + return error; + + if ((service = select_service(action)) == NULL) { + git_error_set(GIT_ERROR_HTTP, "invalid action"); + return -1; + } + + stream = git__calloc(sizeof(http_stream), 1); + GIT_ERROR_CHECK_ALLOC(stream); + + opts.server_certificate_check_cb = connect_opts->callbacks.certificate_check; + opts.server_certificate_check_payload = connect_opts->callbacks.payload; + opts.proxy_certificate_check_cb = connect_opts->proxy_opts.certificate_check; + opts.proxy_certificate_check_payload = connect_opts->proxy_opts.payload; + + if (transport->http_client) { + git_http_client_set_options(transport->http_client, &opts); + } else { + if (git_http_client_new(&transport->http_client, &opts) < 0) + return -1; + } + + stream->service = service; + stream->parent.subtransport = &transport->parent; + + if (service->method == GIT_HTTP_METHOD_GET) { + stream->parent.read = http_stream_read; + } else { + stream->parent.write = http_stream_write; + stream->parent.read = http_stream_read_response; + } + + stream->parent.free = http_stream_free; + + *out = (git_smart_subtransport_stream *)stream; + return 0; +} + +static int http_close(git_smart_subtransport *t) +{ + http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent); + + free_cred(&transport->server.cred); + free_cred(&transport->proxy.cred); + + transport->server.url_cred_presented = false; + transport->proxy.url_cred_presented = false; + + git_net_url_dispose(&transport->server.url); + git_net_url_dispose(&transport->proxy.url); + + return 0; +} + +static void http_free(git_smart_subtransport *t) +{ + http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent); + + git_http_client_free(transport->http_client); + + http_close(t); + git__free(transport); +} + +int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner, void *param) +{ + http_subtransport *transport; + + GIT_UNUSED(param); + + GIT_ASSERT_ARG(out); + + transport = git__calloc(sizeof(http_subtransport), 1); + GIT_ERROR_CHECK_ALLOC(transport); + + transport->owner = (transport_smart *)owner; + transport->parent.action = http_action; + transport->parent.close = http_close; + transport->parent.free = http_free; + + *out = (git_smart_subtransport *) transport; + return 0; +} + +#endif /* !GIT_WINHTTP */ diff --git a/src/libgit2/transports/http.h b/src/libgit2/transports/http.h new file mode 100644 index 0000000..8e8e722 --- /dev/null +++ b/src/libgit2/transports/http.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_transports_http_h__ +#define INCLUDE_transports_http_h__ + +#include "settings.h" +#include "httpclient.h" + +#define GIT_HTTP_REPLAY_MAX 15 + +extern bool git_http__expect_continue; + +GIT_INLINE(int) git_http__user_agent(git_str *buf) +{ + const char *ua = git_libgit2__user_agent(); + + if (!ua) + ua = "libgit2 " LIBGIT2_VERSION; + + return git_str_printf(buf, "git/2.0 (%s)", ua); +} + +#endif diff --git a/src/libgit2/transports/httpclient.c b/src/libgit2/transports/httpclient.c new file mode 100644 index 0000000..a20b594 --- /dev/null +++ b/src/libgit2/transports/httpclient.c @@ -0,0 +1,1593 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" +#include "git2.h" +#include "http_parser.h" +#include "vector.h" +#include "trace.h" +#include "httpclient.h" +#include "http.h" +#include "auth.h" +#include "auth_negotiate.h" +#include "auth_ntlm.h" +#include "git2/sys/credential.h" +#include "net.h" +#include "stream.h" +#include "streams/socket.h" +#include "streams/tls.h" +#include "auth.h" + +static git_http_auth_scheme auth_schemes[] = { + { GIT_HTTP_AUTH_NEGOTIATE, "Negotiate", GIT_CREDENTIAL_DEFAULT, git_http_auth_negotiate }, + { GIT_HTTP_AUTH_NTLM, "NTLM", GIT_CREDENTIAL_USERPASS_PLAINTEXT, git_http_auth_ntlm }, + { GIT_HTTP_AUTH_BASIC, "Basic", GIT_CREDENTIAL_USERPASS_PLAINTEXT, git_http_auth_basic }, +}; + +/* + * Use a 16kb read buffer to match the maximum size of a TLS packet. This + * is critical for compatibility with SecureTransport, which will always do + * a network read on every call, even if it has data buffered to return to + * you. That buffered data may be the _end_ of a keep-alive response, so + * if SecureTransport performs another network read, it will wait until the + * server ultimately times out before it returns that buffered data to you. + * Since SecureTransport only reads a single TLS packet at a time, by + * calling it with a read buffer that is the maximum size of a TLS packet, + * we ensure that it will never buffer. + */ +#define GIT_READ_BUFFER_SIZE (16 * 1024) + +typedef struct { + git_net_url url; + git_stream *stream; + + git_vector auth_challenges; + git_http_auth_context *auth_context; +} git_http_server; + +typedef enum { + PROXY = 1, + SERVER +} git_http_server_t; + +typedef enum { + NONE = 0, + SENDING_REQUEST, + SENDING_BODY, + SENT_REQUEST, + HAS_EARLY_RESPONSE, + READING_RESPONSE, + READING_BODY, + DONE +} http_client_state; + +/* Parser state */ +typedef enum { + PARSE_HEADER_NONE = 0, + PARSE_HEADER_NAME, + PARSE_HEADER_VALUE, + PARSE_HEADER_COMPLETE +} parse_header_state; + +typedef enum { + PARSE_STATUS_OK, + PARSE_STATUS_NO_OUTPUT, + PARSE_STATUS_ERROR +} parse_status; + +typedef struct { + git_http_client *client; + git_http_response *response; + + /* Temporary buffers to avoid extra mallocs */ + git_str parse_header_name; + git_str parse_header_value; + + /* Parser state */ + int error; + parse_status parse_status; + + /* Headers parsing */ + parse_header_state parse_header_state; + + /* Body parsing */ + char *output_buf; /* Caller's output buffer */ + size_t output_size; /* Size of caller's output buffer */ + size_t output_written; /* Bytes we've written to output buffer */ +} http_parser_context; + +/* HTTP client connection */ +struct git_http_client { + git_http_client_options opts; + + /* Are we writing to the proxy or server, and state of the client. */ + git_http_server_t current_server; + http_client_state state; + + http_parser parser; + + git_http_server server; + git_http_server proxy; + + unsigned request_count; + unsigned connected : 1, + proxy_connected : 1, + keepalive : 1, + request_chunked : 1; + + /* Temporary buffers to avoid extra mallocs */ + git_str request_msg; + git_str read_buf; + + /* A subset of information from the request */ + size_t request_body_len, + request_body_remain; + + /* + * When state == HAS_EARLY_RESPONSE, the response of our proxy + * that we have buffered and will deliver during read_response. + */ + git_http_response early_response; +}; + +bool git_http_response_is_redirect(git_http_response *response) +{ + return (response->status == GIT_HTTP_MOVED_PERMANENTLY || + response->status == GIT_HTTP_FOUND || + response->status == GIT_HTTP_SEE_OTHER || + response->status == GIT_HTTP_TEMPORARY_REDIRECT || + response->status == GIT_HTTP_PERMANENT_REDIRECT); +} + +void git_http_response_dispose(git_http_response *response) +{ + if (!response) + return; + + git__free(response->content_type); + git__free(response->location); + + memset(response, 0, sizeof(git_http_response)); +} + +static int on_header_complete(http_parser *parser) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + git_http_client *client = ctx->client; + git_http_response *response = ctx->response; + + git_str *name = &ctx->parse_header_name; + git_str *value = &ctx->parse_header_value; + + if (!strcasecmp("Content-Type", name->ptr)) { + if (response->content_type) { + git_error_set(GIT_ERROR_HTTP, + "multiple content-type headers"); + return -1; + } + + response->content_type = + git__strndup(value->ptr, value->size); + GIT_ERROR_CHECK_ALLOC(ctx->response->content_type); + } else if (!strcasecmp("Content-Length", name->ptr)) { + int64_t len; + + if (response->content_length) { + git_error_set(GIT_ERROR_HTTP, + "multiple content-length headers"); + return -1; + } + + if (git__strntol64(&len, value->ptr, value->size, + NULL, 10) < 0 || len < 0) { + git_error_set(GIT_ERROR_HTTP, + "invalid content-length"); + return -1; + } + + response->content_length = (size_t)len; + } else if (!strcasecmp("Transfer-Encoding", name->ptr) && + !strcasecmp("chunked", value->ptr)) { + ctx->response->chunked = 1; + } else if (!strcasecmp("Proxy-Authenticate", git_str_cstr(name))) { + char *dup = git__strndup(value->ptr, value->size); + GIT_ERROR_CHECK_ALLOC(dup); + + if (git_vector_insert(&client->proxy.auth_challenges, dup) < 0) + return -1; + } else if (!strcasecmp("WWW-Authenticate", name->ptr)) { + char *dup = git__strndup(value->ptr, value->size); + GIT_ERROR_CHECK_ALLOC(dup); + + if (git_vector_insert(&client->server.auth_challenges, dup) < 0) + return -1; + } else if (!strcasecmp("Location", name->ptr)) { + if (response->location) { + git_error_set(GIT_ERROR_HTTP, + "multiple location headers"); + return -1; + } + + response->location = git__strndup(value->ptr, value->size); + GIT_ERROR_CHECK_ALLOC(response->location); + } + + return 0; +} + +static int on_header_field(http_parser *parser, const char *str, size_t len) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + + switch (ctx->parse_header_state) { + /* + * We last saw a header value, process the name/value pair and + * get ready to handle this new name. + */ + case PARSE_HEADER_VALUE: + if (on_header_complete(parser) < 0) + return ctx->parse_status = PARSE_STATUS_ERROR; + + git_str_clear(&ctx->parse_header_name); + git_str_clear(&ctx->parse_header_value); + /* Fall through */ + + case PARSE_HEADER_NONE: + case PARSE_HEADER_NAME: + ctx->parse_header_state = PARSE_HEADER_NAME; + + if (git_str_put(&ctx->parse_header_name, str, len) < 0) + return ctx->parse_status = PARSE_STATUS_ERROR; + + break; + + default: + git_error_set(GIT_ERROR_HTTP, + "header name seen at unexpected time"); + return ctx->parse_status = PARSE_STATUS_ERROR; + } + + return 0; +} + +static int on_header_value(http_parser *parser, const char *str, size_t len) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + + switch (ctx->parse_header_state) { + case PARSE_HEADER_NAME: + case PARSE_HEADER_VALUE: + ctx->parse_header_state = PARSE_HEADER_VALUE; + + if (git_str_put(&ctx->parse_header_value, str, len) < 0) + return ctx->parse_status = PARSE_STATUS_ERROR; + + break; + + default: + git_error_set(GIT_ERROR_HTTP, + "header value seen at unexpected time"); + return ctx->parse_status = PARSE_STATUS_ERROR; + } + + return 0; +} + +GIT_INLINE(bool) challenge_matches_scheme( + const char *challenge, + git_http_auth_scheme *scheme) +{ + const char *scheme_name = scheme->name; + size_t scheme_len = strlen(scheme_name); + + if (!strncasecmp(challenge, scheme_name, scheme_len) && + (challenge[scheme_len] == '\0' || challenge[scheme_len] == ' ')) + return true; + + return false; +} + +static git_http_auth_scheme *scheme_for_challenge(const char *challenge) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(auth_schemes); i++) { + if (challenge_matches_scheme(challenge, &auth_schemes[i])) + return &auth_schemes[i]; + } + + return NULL; +} + +GIT_INLINE(void) collect_authinfo( + unsigned int *schemetypes, + unsigned int *credtypes, + git_vector *challenges) +{ + git_http_auth_scheme *scheme; + const char *challenge; + size_t i; + + *schemetypes = 0; + *credtypes = 0; + + git_vector_foreach(challenges, i, challenge) { + if ((scheme = scheme_for_challenge(challenge)) != NULL) { + *schemetypes |= scheme->type; + *credtypes |= scheme->credtypes; + } + } +} + +static int resend_needed(git_http_client *client, git_http_response *response) +{ + git_http_auth_context *auth_context; + + if (response->status == GIT_HTTP_STATUS_UNAUTHORIZED && + (auth_context = client->server.auth_context) && + auth_context->is_complete && + !auth_context->is_complete(auth_context)) + return 1; + + if (response->status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED && + (auth_context = client->proxy.auth_context) && + auth_context->is_complete && + !auth_context->is_complete(auth_context)) + return 1; + + return 0; +} + +static int on_headers_complete(http_parser *parser) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + + /* Finalize the last seen header */ + switch (ctx->parse_header_state) { + case PARSE_HEADER_VALUE: + if (on_header_complete(parser) < 0) + return ctx->parse_status = PARSE_STATUS_ERROR; + + /* Fall through */ + + case PARSE_HEADER_NONE: + ctx->parse_header_state = PARSE_HEADER_COMPLETE; + break; + + default: + git_error_set(GIT_ERROR_HTTP, + "header completion at unexpected time"); + return ctx->parse_status = PARSE_STATUS_ERROR; + } + + ctx->response->status = parser->status_code; + ctx->client->keepalive = http_should_keep_alive(parser); + + /* Prepare for authentication */ + collect_authinfo(&ctx->response->server_auth_schemetypes, + &ctx->response->server_auth_credtypes, + &ctx->client->server.auth_challenges); + collect_authinfo(&ctx->response->proxy_auth_schemetypes, + &ctx->response->proxy_auth_credtypes, + &ctx->client->proxy.auth_challenges); + + ctx->response->resend_credentials = resend_needed(ctx->client, + ctx->response); + + /* Stop parsing. */ + http_parser_pause(parser, 1); + + if (ctx->response->content_type || ctx->response->chunked) + ctx->client->state = READING_BODY; + else + ctx->client->state = DONE; + + return 0; +} + +static int on_body(http_parser *parser, const char *buf, size_t len) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + size_t max_len; + + /* Saw data when we expected not to (eg, in consume_response_body) */ + if (ctx->output_buf == NULL || ctx->output_size == 0) { + ctx->parse_status = PARSE_STATUS_NO_OUTPUT; + return 0; + } + + GIT_ASSERT(ctx->output_size >= ctx->output_written); + + max_len = min(ctx->output_size - ctx->output_written, len); + max_len = min(max_len, INT_MAX); + + memcpy(ctx->output_buf + ctx->output_written, buf, max_len); + ctx->output_written += max_len; + + return 0; +} + +static int on_message_complete(http_parser *parser) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + + ctx->client->state = DONE; + return 0; +} + +GIT_INLINE(int) stream_write( + git_http_server *server, + const char *data, + size_t len) +{ + git_trace(GIT_TRACE_TRACE, + "Sending request:\n%.*s", (int)len, data); + + return git_stream__write_full(server->stream, data, len, 0); +} + +GIT_INLINE(int) client_write_request(git_http_client *client) +{ + git_stream *stream = client->current_server == PROXY ? + client->proxy.stream : client->server.stream; + + git_trace(GIT_TRACE_TRACE, + "Sending request:\n%.*s", + (int)client->request_msg.size, client->request_msg.ptr); + + return git_stream__write_full(stream, + client->request_msg.ptr, + client->request_msg.size, + 0); +} + +static const char *name_for_method(git_http_method method) +{ + switch (method) { + case GIT_HTTP_METHOD_GET: + return "GET"; + case GIT_HTTP_METHOD_POST: + return "POST"; + case GIT_HTTP_METHOD_CONNECT: + return "CONNECT"; + } + + return NULL; +} + +/* + * Find the scheme that is suitable for the given credentials, based on the + * server's auth challenges. + */ +static bool best_scheme_and_challenge( + git_http_auth_scheme **scheme_out, + const char **challenge_out, + git_vector *challenges, + git_credential *credentials) +{ + const char *challenge; + size_t i, j; + + for (i = 0; i < ARRAY_SIZE(auth_schemes); i++) { + git_vector_foreach(challenges, j, challenge) { + git_http_auth_scheme *scheme = &auth_schemes[i]; + + if (challenge_matches_scheme(challenge, scheme) && + (scheme->credtypes & credentials->credtype)) { + *scheme_out = scheme; + *challenge_out = challenge; + return true; + } + } + } + + return false; +} + +/* + * Find the challenge from the server for our current auth context. + */ +static const char *challenge_for_context( + git_vector *challenges, + git_http_auth_context *auth_ctx) +{ + const char *challenge; + size_t i, j; + + for (i = 0; i < ARRAY_SIZE(auth_schemes); i++) { + if (auth_schemes[i].type == auth_ctx->type) { + git_http_auth_scheme *scheme = &auth_schemes[i]; + + git_vector_foreach(challenges, j, challenge) { + if (challenge_matches_scheme(challenge, scheme)) + return challenge; + } + } + } + + return NULL; +} + +static const char *init_auth_context( + git_http_server *server, + git_vector *challenges, + git_credential *credentials) +{ + git_http_auth_scheme *scheme; + const char *challenge; + int error; + + if (!best_scheme_and_challenge(&scheme, &challenge, challenges, credentials)) { + git_error_set(GIT_ERROR_HTTP, "could not find appropriate mechanism for credentials"); + return NULL; + } + + error = scheme->init_context(&server->auth_context, &server->url); + + if (error == GIT_PASSTHROUGH) { + git_error_set(GIT_ERROR_HTTP, "'%s' authentication is not supported", scheme->name); + return NULL; + } + + return challenge; +} + +static void free_auth_context(git_http_server *server) +{ + if (!server->auth_context) + return; + + if (server->auth_context->free) + server->auth_context->free(server->auth_context); + + server->auth_context = NULL; +} + +static int apply_credentials( + git_str *buf, + git_http_server *server, + const char *header_name, + git_credential *credentials) +{ + git_http_auth_context *auth = server->auth_context; + git_vector *challenges = &server->auth_challenges; + const char *challenge; + git_str token = GIT_STR_INIT; + int error = 0; + + /* We've started a new request without creds; free the context. */ + if (auth && !credentials) { + free_auth_context(server); + return 0; + } + + /* We haven't authenticated, nor were we asked to. Nothing to do. */ + if (!auth && !git_vector_length(challenges)) + return 0; + + if (!auth) { + challenge = init_auth_context(server, challenges, credentials); + auth = server->auth_context; + + if (!challenge || !auth) { + error = -1; + goto done; + } + } else if (auth->set_challenge) { + challenge = challenge_for_context(challenges, auth); + } + + if (auth->set_challenge && challenge && + (error = auth->set_challenge(auth, challenge)) < 0) + goto done; + + if ((error = auth->next_token(&token, auth, credentials)) < 0) + goto done; + + if (auth->is_complete && auth->is_complete(auth)) { + /* + * If we're done with an auth mechanism with connection affinity, + * we don't need to send any more headers and can dispose the context. + */ + if (auth->connection_affinity) + free_auth_context(server); + } else if (!token.size) { + git_error_set(GIT_ERROR_HTTP, "failed to respond to authentication challenge"); + error = GIT_EAUTH; + goto done; + } + + if (token.size > 0) + error = git_str_printf(buf, "%s: %s\r\n", header_name, token.ptr); + +done: + git_str_dispose(&token); + return error; +} + +GIT_INLINE(int) apply_server_credentials( + git_str *buf, + git_http_client *client, + git_http_request *request) +{ + return apply_credentials(buf, + &client->server, + "Authorization", + request->credentials); +} + +GIT_INLINE(int) apply_proxy_credentials( + git_str *buf, + git_http_client *client, + git_http_request *request) +{ + return apply_credentials(buf, + &client->proxy, + "Proxy-Authorization", + request->proxy_credentials); +} + +static int puts_host_and_port(git_str *buf, git_net_url *url, bool force_port) +{ + bool ipv6 = git_net_url_is_ipv6(url); + + if (ipv6) + git_str_putc(buf, '['); + + git_str_puts(buf, url->host); + + if (ipv6) + git_str_putc(buf, ']'); + + if (force_port || !git_net_url_is_default_port(url)) { + git_str_putc(buf, ':'); + git_str_puts(buf, url->port); + } + + return git_str_oom(buf) ? -1 : 0; +} + +static int generate_connect_request( + git_http_client *client, + git_http_request *request) +{ + git_str *buf; + int error; + + git_str_clear(&client->request_msg); + buf = &client->request_msg; + + git_str_puts(buf, "CONNECT "); + puts_host_and_port(buf, &client->server.url, true); + git_str_puts(buf, " HTTP/1.1\r\n"); + + git_str_puts(buf, "User-Agent: "); + git_http__user_agent(buf); + git_str_puts(buf, "\r\n"); + + git_str_puts(buf, "Host: "); + puts_host_and_port(buf, &client->server.url, true); + git_str_puts(buf, "\r\n"); + + if ((error = apply_proxy_credentials(buf, client, request) < 0)) + return -1; + + git_str_puts(buf, "\r\n"); + + return git_str_oom(buf) ? -1 : 0; +} + +static bool use_connect_proxy(git_http_client *client) +{ + return client->proxy.url.host && !strcmp(client->server.url.scheme, "https"); +} + +static int generate_request( + git_http_client *client, + git_http_request *request) +{ + git_str *buf; + size_t i; + int error; + + GIT_ASSERT_ARG(client); + GIT_ASSERT_ARG(request); + + git_str_clear(&client->request_msg); + buf = &client->request_msg; + + /* GET|POST path HTTP/1.1 */ + git_str_puts(buf, name_for_method(request->method)); + git_str_putc(buf, ' '); + + if (request->proxy && strcmp(request->url->scheme, "https")) + git_net_url_fmt(buf, request->url); + else + git_net_url_fmt_path(buf, request->url); + + git_str_puts(buf, " HTTP/1.1\r\n"); + + git_str_puts(buf, "User-Agent: "); + git_http__user_agent(buf); + git_str_puts(buf, "\r\n"); + + git_str_puts(buf, "Host: "); + puts_host_and_port(buf, request->url, false); + git_str_puts(buf, "\r\n"); + + if (request->accept) + git_str_printf(buf, "Accept: %s\r\n", request->accept); + else + git_str_puts(buf, "Accept: */*\r\n"); + + if (request->content_type) + git_str_printf(buf, "Content-Type: %s\r\n", + request->content_type); + + if (request->chunked) + git_str_puts(buf, "Transfer-Encoding: chunked\r\n"); + + if (request->content_length > 0) + git_str_printf(buf, "Content-Length: %"PRIuZ "\r\n", + request->content_length); + + if (request->expect_continue) + git_str_printf(buf, "Expect: 100-continue\r\n"); + + if ((error = apply_server_credentials(buf, client, request)) < 0 || + (!use_connect_proxy(client) && + (error = apply_proxy_credentials(buf, client, request)) < 0)) + return error; + + if (request->custom_headers) { + for (i = 0; i < request->custom_headers->count; i++) { + const char *hdr = request->custom_headers->strings[i]; + + if (hdr) + git_str_printf(buf, "%s\r\n", hdr); + } + } + + git_str_puts(buf, "\r\n"); + + if (git_str_oom(buf)) + return -1; + + return 0; +} + +static int check_certificate( + git_stream *stream, + git_net_url *url, + int is_valid, + git_transport_certificate_check_cb cert_cb, + void *cert_cb_payload) +{ + git_cert *cert; + git_error_state last_error = {0}; + int error; + + if ((error = git_stream_certificate(&cert, stream)) < 0) + return error; + + git_error_state_capture(&last_error, GIT_ECERTIFICATE); + + error = cert_cb(cert, is_valid, url->host, cert_cb_payload); + + if (error == GIT_PASSTHROUGH && !is_valid) + return git_error_state_restore(&last_error); + else if (error == GIT_PASSTHROUGH) + error = 0; + else if (error && !git_error_last()) + git_error_set(GIT_ERROR_HTTP, + "user rejected certificate for %s", url->host); + + git_error_state_free(&last_error); + return error; +} + +static int server_connect_stream( + git_http_server *server, + git_transport_certificate_check_cb cert_cb, + void *cb_payload) +{ + int error; + + GIT_ERROR_CHECK_VERSION(server->stream, GIT_STREAM_VERSION, "git_stream"); + + error = git_stream_connect(server->stream); + + if (error && error != GIT_ECERTIFICATE) + return error; + + if (git_stream_is_encrypted(server->stream) && cert_cb != NULL) + error = check_certificate(server->stream, &server->url, !error, + cert_cb, cb_payload); + + return error; +} + +static void reset_auth_connection(git_http_server *server) +{ + /* + * If we've authenticated and we're doing "normal" + * authentication with a request affinity (Basic, Digest) + * then we want to _keep_ our context, since authentication + * survives even through non-keep-alive connections. If + * we've authenticated and we're doing connection-based + * authentication (NTLM, Negotiate) - indicated by the presence + * of an `is_complete` callback - then we need to restart + * authentication on a new connection. + */ + + if (server->auth_context && + server->auth_context->connection_affinity) + free_auth_context(server); +} + +/* + * Updates the server data structure with the new URL; returns 1 if the server + * has changed and we need to reconnect, returns 0 otherwise. + */ +GIT_INLINE(int) server_setup_from_url( + git_http_server *server, + git_net_url *url) +{ + GIT_ASSERT_ARG(url); + GIT_ASSERT_ARG(url->scheme); + GIT_ASSERT_ARG(url->host); + GIT_ASSERT_ARG(url->port); + + if (!server->url.scheme || strcmp(server->url.scheme, url->scheme) || + !server->url.host || strcmp(server->url.host, url->host) || + !server->url.port || strcmp(server->url.port, url->port)) { + git__free(server->url.scheme); + git__free(server->url.host); + git__free(server->url.port); + + server->url.scheme = git__strdup(url->scheme); + GIT_ERROR_CHECK_ALLOC(server->url.scheme); + + server->url.host = git__strdup(url->host); + GIT_ERROR_CHECK_ALLOC(server->url.host); + + server->url.port = git__strdup(url->port); + GIT_ERROR_CHECK_ALLOC(server->url.port); + + return 1; + } + + return 0; +} + +static void reset_parser(git_http_client *client) +{ + http_parser_init(&client->parser, HTTP_RESPONSE); +} + +static int setup_hosts( + git_http_client *client, + git_http_request *request) +{ + int ret, diff = 0; + + GIT_ASSERT_ARG(client); + GIT_ASSERT_ARG(request); + + GIT_ASSERT(request->url); + + if ((ret = server_setup_from_url(&client->server, request->url)) < 0) + return ret; + + diff |= ret; + + if (request->proxy && + (ret = server_setup_from_url(&client->proxy, request->proxy)) < 0) + return ret; + + diff |= ret; + + if (diff) { + free_auth_context(&client->server); + free_auth_context(&client->proxy); + + client->connected = 0; + } + + return 0; +} + +GIT_INLINE(int) server_create_stream(git_http_server *server) +{ + git_net_url *url = &server->url; + + if (strcasecmp(url->scheme, "https") == 0) + return git_tls_stream_new(&server->stream, url->host, url->port); + else if (strcasecmp(url->scheme, "http") == 0) + return git_socket_stream_new(&server->stream, url->host, url->port); + + git_error_set(GIT_ERROR_HTTP, "unknown http scheme '%s'", url->scheme); + return -1; +} + +GIT_INLINE(void) save_early_response( + git_http_client *client, + git_http_response *response) +{ + /* Buffer the response so we can return it in read_response */ + client->state = HAS_EARLY_RESPONSE; + + memcpy(&client->early_response, response, sizeof(git_http_response)); + memset(response, 0, sizeof(git_http_response)); +} + +static int proxy_connect( + git_http_client *client, + git_http_request *request) +{ + git_http_response response = {0}; + int error; + + if (!client->proxy_connected || !client->keepalive) { + git_trace(GIT_TRACE_DEBUG, "Connecting to proxy %s port %s", + client->proxy.url.host, client->proxy.url.port); + + if ((error = server_create_stream(&client->proxy)) < 0 || + (error = server_connect_stream(&client->proxy, + client->opts.proxy_certificate_check_cb, + client->opts.proxy_certificate_check_payload)) < 0) + goto done; + + client->proxy_connected = 1; + } + + client->current_server = PROXY; + client->state = SENDING_REQUEST; + + if ((error = generate_connect_request(client, request)) < 0 || + (error = client_write_request(client)) < 0) + goto done; + + client->state = SENT_REQUEST; + + if ((error = git_http_client_read_response(&response, client)) < 0 || + (error = git_http_client_skip_body(client)) < 0) + goto done; + + GIT_ASSERT(client->state == DONE); + + if (response.status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED) { + save_early_response(client, &response); + + error = GIT_RETRY; + goto done; + } else if (response.status != GIT_HTTP_STATUS_OK) { + git_error_set(GIT_ERROR_HTTP, "proxy returned unexpected status: %d", response.status); + error = -1; + goto done; + } + + reset_parser(client); + client->state = NONE; + +done: + git_http_response_dispose(&response); + return error; +} + +static int server_connect(git_http_client *client) +{ + git_net_url *url = &client->server.url; + git_transport_certificate_check_cb cert_cb; + void *cert_payload; + int error; + + client->current_server = SERVER; + + if (client->proxy.stream) + error = git_tls_stream_wrap(&client->server.stream, client->proxy.stream, url->host); + else + error = server_create_stream(&client->server); + + if (error < 0) + goto done; + + cert_cb = client->opts.server_certificate_check_cb; + cert_payload = client->opts.server_certificate_check_payload; + + error = server_connect_stream(&client->server, cert_cb, cert_payload); + +done: + return error; +} + +GIT_INLINE(void) close_stream(git_http_server *server) +{ + if (server->stream) { + git_stream_close(server->stream); + git_stream_free(server->stream); + server->stream = NULL; + } +} + +static int http_client_connect( + git_http_client *client, + git_http_request *request) +{ + bool use_proxy = false; + int error; + + if ((error = setup_hosts(client, request)) < 0) + goto on_error; + + /* We're connected to our destination server; no need to reconnect */ + if (client->connected && client->keepalive && + (client->state == NONE || client->state == DONE)) + return 0; + + client->connected = 0; + client->request_count = 0; + + close_stream(&client->server); + reset_auth_connection(&client->server); + + reset_parser(client); + + /* Reconnect to the proxy if necessary. */ + use_proxy = use_connect_proxy(client); + + if (use_proxy) { + if (!client->proxy_connected || !client->keepalive || + (client->state != NONE && client->state != DONE)) { + close_stream(&client->proxy); + reset_auth_connection(&client->proxy); + + client->proxy_connected = 0; + } + + if ((error = proxy_connect(client, request)) < 0) + goto on_error; + } + + git_trace(GIT_TRACE_DEBUG, "Connecting to remote %s port %s", + client->server.url.host, client->server.url.port); + + if ((error = server_connect(client)) < 0) + goto on_error; + + client->connected = 1; + return error; + +on_error: + if (error != GIT_RETRY) + close_stream(&client->proxy); + + close_stream(&client->server); + return error; +} + +GIT_INLINE(int) client_read(git_http_client *client) +{ + http_parser_context *parser_context = client->parser.data; + git_stream *stream; + char *buf = client->read_buf.ptr + client->read_buf.size; + size_t max_len; + ssize_t read_len; + + stream = client->current_server == PROXY ? + client->proxy.stream : client->server.stream; + + /* + * We use a git_str for convenience, but statically allocate it and + * don't resize. Limit our consumption to INT_MAX since calling + * functions use an int return type to return number of bytes read. + */ + max_len = client->read_buf.asize - client->read_buf.size; + max_len = min(max_len, INT_MAX); + + if (parser_context->output_size) + max_len = min(max_len, parser_context->output_size); + + if (max_len == 0) { + git_error_set(GIT_ERROR_HTTP, "no room in output buffer"); + return -1; + } + + read_len = git_stream_read(stream, buf, max_len); + + if (read_len >= 0) { + client->read_buf.size += read_len; + + git_trace(GIT_TRACE_TRACE, "Received:\n%.*s", + (int)read_len, buf); + } + + return (int)read_len; +} + +static bool parser_settings_initialized; +static http_parser_settings parser_settings; + +GIT_INLINE(http_parser_settings *) http_client_parser_settings(void) +{ + if (!parser_settings_initialized) { + parser_settings.on_header_field = on_header_field; + parser_settings.on_header_value = on_header_value; + parser_settings.on_headers_complete = on_headers_complete; + parser_settings.on_body = on_body; + parser_settings.on_message_complete = on_message_complete; + + parser_settings_initialized = true; + } + + return &parser_settings; +} + +GIT_INLINE(int) client_read_and_parse(git_http_client *client) +{ + http_parser *parser = &client->parser; + http_parser_context *ctx = (http_parser_context *) parser->data; + unsigned char http_errno; + int read_len; + size_t parsed_len; + + /* + * If we have data in our read buffer, that means we stopped early + * when parsing headers. Use the data in the read buffer instead of + * reading more from the socket. + */ + if (!client->read_buf.size && (read_len = client_read(client)) < 0) + return read_len; + + parsed_len = http_parser_execute(parser, + http_client_parser_settings(), + client->read_buf.ptr, + client->read_buf.size); + http_errno = client->parser.http_errno; + + if (parsed_len > INT_MAX) { + git_error_set(GIT_ERROR_HTTP, "unexpectedly large parse"); + return -1; + } + + if (ctx->parse_status == PARSE_STATUS_ERROR) { + client->connected = 0; + return ctx->error ? ctx->error : -1; + } + + /* + * If we finished reading the headers or body, we paused parsing. + * Otherwise the parser will start filling the body, or even parse + * a new response if the server pipelined us multiple responses. + * (This can happen in response to an expect/continue request, + * where the server gives you a 100 and 200 simultaneously.) + */ + if (http_errno == HPE_PAUSED) { + /* + * http-parser has a "feature" where it will not deliver the + * final byte when paused in a callback. Consume that byte. + * https://github.com/nodejs/http-parser/issues/97 + */ + GIT_ASSERT(client->read_buf.size > parsed_len); + + http_parser_pause(parser, 0); + + parsed_len += http_parser_execute(parser, + http_client_parser_settings(), + client->read_buf.ptr + parsed_len, + 1); + } + + /* Most failures will be reported in http_errno */ + else if (parser->http_errno != HPE_OK) { + git_error_set(GIT_ERROR_HTTP, "http parser error: %s", + http_errno_description(http_errno)); + return -1; + } + + /* Otherwise we should have consumed the entire buffer. */ + else if (parsed_len != client->read_buf.size) { + git_error_set(GIT_ERROR_HTTP, + "http parser did not consume entire buffer: %s", + http_errno_description(http_errno)); + return -1; + } + + /* recv returned 0, the server hung up on us */ + else if (!parsed_len) { + git_error_set(GIT_ERROR_HTTP, "unexpected EOF"); + return -1; + } + + git_str_consume_bytes(&client->read_buf, parsed_len); + + return (int)parsed_len; +} + +/* + * See if we've consumed the entire response body. If the client was + * reading the body but did not consume it entirely, it's possible that + * they knew that the stream had finished (in a git response, seeing a + * final flush) and stopped reading. But if the response was chunked, + * we may have not consumed the final chunk marker. Consume it to + * ensure that we don't have it waiting in our socket. If there's + * more than just a chunk marker, close the connection. + */ +static void complete_response_body(git_http_client *client) +{ + http_parser_context parser_context = {0}; + + /* If we're not keeping alive, don't bother. */ + if (!client->keepalive) { + client->connected = 0; + goto done; + } + + parser_context.client = client; + client->parser.data = &parser_context; + + /* If there was an error, just close the connection. */ + if (client_read_and_parse(client) < 0 || + parser_context.error != HPE_OK || + (parser_context.parse_status != PARSE_STATUS_OK && + parser_context.parse_status != PARSE_STATUS_NO_OUTPUT)) { + git_error_clear(); + client->connected = 0; + } + +done: + git_str_clear(&client->read_buf); +} + +int git_http_client_send_request( + git_http_client *client, + git_http_request *request) +{ + git_http_response response = {0}; + int error = -1; + + GIT_ASSERT_ARG(client); + GIT_ASSERT_ARG(request); + + /* If the client did not finish reading, clean up the stream. */ + if (client->state == READING_BODY) + complete_response_body(client); + + /* If we're waiting for proxy auth, don't sending more requests. */ + if (client->state == HAS_EARLY_RESPONSE) + return 0; + + if (git_trace_level() >= GIT_TRACE_DEBUG) { + git_str url = GIT_STR_INIT; + git_net_url_fmt(&url, request->url); + git_trace(GIT_TRACE_DEBUG, "Sending %s request to %s", + name_for_method(request->method), + url.ptr ? url.ptr : ""); + git_str_dispose(&url); + } + + if ((error = http_client_connect(client, request)) < 0 || + (error = generate_request(client, request)) < 0 || + (error = client_write_request(client)) < 0) + goto done; + + client->state = SENT_REQUEST; + + if (request->expect_continue) { + if ((error = git_http_client_read_response(&response, client)) < 0 || + (error = git_http_client_skip_body(client)) < 0) + goto done; + + error = 0; + + if (response.status != GIT_HTTP_STATUS_CONTINUE) { + save_early_response(client, &response); + goto done; + } + } + + if (request->content_length || request->chunked) { + client->state = SENDING_BODY; + client->request_body_len = request->content_length; + client->request_body_remain = request->content_length; + client->request_chunked = request->chunked; + } + + reset_parser(client); + +done: + if (error == GIT_RETRY) + error = 0; + + git_http_response_dispose(&response); + return error; +} + +bool git_http_client_has_response(git_http_client *client) +{ + return (client->state == HAS_EARLY_RESPONSE || + client->state > SENT_REQUEST); +} + +int git_http_client_send_body( + git_http_client *client, + const char *buffer, + size_t buffer_len) +{ + git_http_server *server; + git_str hdr = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(client); + + /* If we're waiting for proxy auth, don't sending more requests. */ + if (client->state == HAS_EARLY_RESPONSE) + return 0; + + if (client->state != SENDING_BODY) { + git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); + return -1; + } + + if (!buffer_len) + return 0; + + server = &client->server; + + if (client->request_body_len) { + GIT_ASSERT(buffer_len <= client->request_body_remain); + + if ((error = stream_write(server, buffer, buffer_len)) < 0) + goto done; + + client->request_body_remain -= buffer_len; + } else { + if ((error = git_str_printf(&hdr, "%" PRIxZ "\r\n", buffer_len)) < 0 || + (error = stream_write(server, hdr.ptr, hdr.size)) < 0 || + (error = stream_write(server, buffer, buffer_len)) < 0 || + (error = stream_write(server, "\r\n", 2)) < 0) + goto done; + } + +done: + git_str_dispose(&hdr); + return error; +} + +static int complete_request(git_http_client *client) +{ + int error = 0; + + GIT_ASSERT_ARG(client); + GIT_ASSERT(client->state == SENDING_BODY); + + if (client->request_body_len && client->request_body_remain) { + git_error_set(GIT_ERROR_HTTP, "truncated write"); + error = -1; + } else if (client->request_chunked) { + error = stream_write(&client->server, "0\r\n\r\n", 5); + } + + client->state = SENT_REQUEST; + return error; +} + +int git_http_client_read_response( + git_http_response *response, + git_http_client *client) +{ + http_parser_context parser_context = {0}; + int error; + + GIT_ASSERT_ARG(response); + GIT_ASSERT_ARG(client); + + if (client->state == SENDING_BODY) { + if ((error = complete_request(client)) < 0) + goto done; + } + + if (client->state == HAS_EARLY_RESPONSE) { + memcpy(response, &client->early_response, sizeof(git_http_response)); + memset(&client->early_response, 0, sizeof(git_http_response)); + client->state = DONE; + return 0; + } + + if (client->state != SENT_REQUEST) { + git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); + error = -1; + goto done; + } + + git_http_response_dispose(response); + + if (client->current_server == PROXY) { + git_vector_free_deep(&client->proxy.auth_challenges); + } else if(client->current_server == SERVER) { + git_vector_free_deep(&client->server.auth_challenges); + } + + client->state = READING_RESPONSE; + client->keepalive = 0; + client->parser.data = &parser_context; + + parser_context.client = client; + parser_context.response = response; + + while (client->state == READING_RESPONSE) { + if ((error = client_read_and_parse(client)) < 0) + goto done; + } + + GIT_ASSERT(client->state == READING_BODY || client->state == DONE); + +done: + git_str_dispose(&parser_context.parse_header_name); + git_str_dispose(&parser_context.parse_header_value); + + return error; +} + +int git_http_client_read_body( + git_http_client *client, + char *buffer, + size_t buffer_size) +{ + http_parser_context parser_context = {0}; + int error = 0; + + if (client->state == DONE) + return 0; + + if (client->state != READING_BODY) { + git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); + return -1; + } + + /* + * Now we'll read from the socket and http_parser will pipeline the + * data directly to the client. + */ + + parser_context.client = client; + parser_context.output_buf = buffer; + parser_context.output_size = buffer_size; + + client->parser.data = &parser_context; + + /* + * Clients expect to get a non-zero amount of data from us, + * so we either block until we have data to return, until we + * hit EOF or there's an error. Do this in a loop, since we + * may end up reading only some stream metadata (like chunk + * information). + */ + while (!parser_context.output_written) { + error = client_read_and_parse(client); + + if (error <= 0) + goto done; + + if (client->state == DONE) + break; + } + + GIT_ASSERT(parser_context.output_written <= INT_MAX); + error = (int)parser_context.output_written; + +done: + if (error < 0) + client->connected = 0; + + return error; +} + +int git_http_client_skip_body(git_http_client *client) +{ + http_parser_context parser_context = {0}; + int error; + + if (client->state == DONE) + return 0; + + if (client->state != READING_BODY) { + git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); + return -1; + } + + parser_context.client = client; + client->parser.data = &parser_context; + + do { + error = client_read_and_parse(client); + + if (parser_context.error != HPE_OK || + (parser_context.parse_status != PARSE_STATUS_OK && + parser_context.parse_status != PARSE_STATUS_NO_OUTPUT)) { + git_error_set(GIT_ERROR_HTTP, + "unexpected data handled in callback"); + error = -1; + } + } while (error >= 0 && client->state != DONE); + + if (error < 0) + client->connected = 0; + + return error; +} + +/* + * Create an http_client capable of communicating with the given remote + * host. + */ +int git_http_client_new( + git_http_client **out, + git_http_client_options *opts) +{ + git_http_client *client; + + GIT_ASSERT_ARG(out); + + client = git__calloc(1, sizeof(git_http_client)); + GIT_ERROR_CHECK_ALLOC(client); + + git_str_init(&client->read_buf, GIT_READ_BUFFER_SIZE); + GIT_ERROR_CHECK_ALLOC(client->read_buf.ptr); + + if (opts) + memcpy(&client->opts, opts, sizeof(git_http_client_options)); + + *out = client; + return 0; +} + +/* Update the options of an existing httpclient instance. */ +void git_http_client_set_options( + git_http_client *client, + git_http_client_options *opts) +{ + if (opts) + memcpy(&client->opts, opts, sizeof(git_http_client_options)); +} + +GIT_INLINE(void) http_server_close(git_http_server *server) +{ + if (server->stream) { + git_stream_close(server->stream); + git_stream_free(server->stream); + server->stream = NULL; + } + + git_net_url_dispose(&server->url); + + git_vector_free_deep(&server->auth_challenges); + free_auth_context(server); +} + +static void http_client_close(git_http_client *client) +{ + http_server_close(&client->server); + http_server_close(&client->proxy); + + git_str_dispose(&client->request_msg); + + client->state = 0; + client->request_count = 0; + client->connected = 0; + client->keepalive = 0; +} + +void git_http_client_free(git_http_client *client) +{ + if (!client) + return; + + http_client_close(client); + git_str_dispose(&client->read_buf); + git__free(client); +} diff --git a/src/libgit2/transports/httpclient.h b/src/libgit2/transports/httpclient.h new file mode 100644 index 0000000..22c4dd0 --- /dev/null +++ b/src/libgit2/transports/httpclient.h @@ -0,0 +1,200 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_transports_httpclient_h__ +#define INCLUDE_transports_httpclient_h__ + +#include "common.h" +#include "net.h" + +#define GIT_HTTP_STATUS_CONTINUE 100 +#define GIT_HTTP_STATUS_OK 200 +#define GIT_HTTP_MOVED_PERMANENTLY 301 +#define GIT_HTTP_FOUND 302 +#define GIT_HTTP_SEE_OTHER 303 +#define GIT_HTTP_TEMPORARY_REDIRECT 307 +#define GIT_HTTP_PERMANENT_REDIRECT 308 +#define GIT_HTTP_STATUS_UNAUTHORIZED 401 +#define GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED 407 + +typedef struct git_http_client git_http_client; + +/** Method for the HTTP request */ +typedef enum { + GIT_HTTP_METHOD_GET, + GIT_HTTP_METHOD_POST, + GIT_HTTP_METHOD_CONNECT +} git_http_method; + +/** An HTTP request */ +typedef struct { + git_http_method method; /**< Method for the request */ + git_net_url *url; /**< Full request URL */ + git_net_url *proxy; /**< Proxy to use */ + + /* Headers */ + const char *accept; /**< Contents of the Accept header */ + const char *content_type; /**< Content-Type header (for POST) */ + git_credential *credentials; /**< Credentials to authenticate with */ + git_credential *proxy_credentials; /**< Credentials for proxy */ + git_strarray *custom_headers; /**< Additional headers to deliver */ + + /* To POST a payload, either set content_length OR set chunked. */ + size_t content_length; /**< Length of the POST body */ + unsigned chunked : 1, /**< Post with chunking */ + expect_continue : 1; /**< Use expect/continue negotiation */ +} git_http_request; + +typedef struct { + int status; + + /* Headers */ + char *content_type; + size_t content_length; + char *location; + + /* Authentication headers */ + unsigned server_auth_schemetypes; /**< Schemes requested by remote */ + unsigned server_auth_credtypes; /**< Supported cred types for remote */ + + unsigned proxy_auth_schemetypes; /**< Schemes requested by proxy */ + unsigned proxy_auth_credtypes; /**< Supported cred types for proxy */ + + unsigned chunked : 1, /**< Response body is chunked */ + resend_credentials : 1; /**< Resend with authentication */ +} git_http_response; + +typedef struct { + /** Certificate check callback for the remote */ + git_transport_certificate_check_cb server_certificate_check_cb; + void *server_certificate_check_payload; + + /** Certificate check callback for the proxy */ + git_transport_certificate_check_cb proxy_certificate_check_cb; + void *proxy_certificate_check_payload; +} git_http_client_options; + +/** + * Create a new httpclient instance with the given options. + * + * @param out pointer to receive the new instance + * @param opts options to create the client with or NULL for defaults + */ +extern int git_http_client_new( + git_http_client **out, + git_http_client_options *opts); + +/** + * Update the options of an existing httpclient instance. + * + * @param client the httpclient instance to modify + * @param opts new options or NULL to keep existing options + */ +extern void git_http_client_set_options( + git_http_client *client, + git_http_client_options *opts); + +/* + * Sends a request to the host specified by the request URL. If the + * method is POST, either the content_length or the chunked flag must + * be specified. The body should be provided in subsequent calls to + * git_http_client_send_body. + * + * @param client the client to write the request to + * @param request the request to send + */ +extern int git_http_client_send_request( + git_http_client *client, + git_http_request *request); + +/* + * After sending a request, there may already be a response to read -- + * either because there was a non-continue response to an expect: continue + * request, or because the server pipelined a response to us before we even + * sent the request. Examine the state. + * + * @param client the client to examine + * @return true if there's already a response to read, false otherwise + */ +extern bool git_http_client_has_response(git_http_client *client); + +/** + * Sends the given buffer to the remote as part of the request body. The + * request must have specified either a content_length or the chunked flag. + * + * @param client the client to write the request body to + * @param buffer the request body + * @param buffer_len number of bytes of the buffer to send + */ +extern int git_http_client_send_body( + git_http_client *client, + const char *buffer, + size_t buffer_len); + +/** + * Reads the headers of a response to a request. This will consume the + * entirety of the headers of a response from the server. The body (if any) + * can be read by calling git_http_client_read_body. Callers must free + * the response with git_http_response_dispose. + * + * @param response pointer to the response object to fill + * @param client the client to read the response from + */ +extern int git_http_client_read_response( + git_http_response *response, + git_http_client *client); + +/** + * Reads some or all of the body of a response. At most buffer_size (or + * INT_MAX) bytes will be read and placed into the buffer provided. The + * number of bytes read will be returned, or 0 to indicate that the end of + * the body has been read. + * + * @param client the client to read the response from + * @param buffer pointer to the buffer to fill + * @param buffer_size the maximum number of bytes to read + * @return the number of bytes read, 0 on end of body, or error code + */ +extern int git_http_client_read_body( + git_http_client *client, + char *buffer, + size_t buffer_size); + +/** + * Reads all of the (remainder of the) body of the response and ignores it. + * None of the data from the body will be returned to the caller. + * + * @param client the client to read the response from + * @return 0 or an error code + */ +extern int git_http_client_skip_body(git_http_client *client); + +/** + * Examines the status code of the response to determine if it is a + * redirect of any type (eg, 301, 302, etc). + * + * @param response the response to inspect + * @return true if the response is a redirect, false otherwise + */ +extern bool git_http_response_is_redirect(git_http_response *response); + +/** + * Frees any memory associated with the response. + * + * @param response the response to free + */ +extern void git_http_response_dispose(git_http_response *response); + +/** + * Frees any memory associated with the client. If any sockets are open, + * they will be closed. + * + * @param client the client to free + */ +extern void git_http_client_free(git_http_client *client); + +#endif diff --git a/src/libgit2/transports/local.c b/src/libgit2/transports/local.c new file mode 100644 index 0000000..64c21af --- /dev/null +++ b/src/libgit2/transports/local.c @@ -0,0 +1,777 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "pack-objects.h" +#include "refs.h" +#include "posix.h" +#include "fs_path.h" +#include "repository.h" +#include "odb.h" +#include "push.h" +#include "remote.h" +#include "proxy.h" + +#include "git2/types.h" +#include "git2/net.h" +#include "git2/repository.h" +#include "git2/object.h" +#include "git2/tag.h" +#include "git2/transport.h" +#include "git2/revwalk.h" +#include "git2/odb_backend.h" +#include "git2/pack.h" +#include "git2/commit.h" +#include "git2/revparse.h" +#include "git2/sys/remote.h" + +typedef struct { + git_transport parent; + git_remote *owner; + char *url; + int direction; + git_atomic32 cancelled; + git_repository *repo; + git_remote_connect_options connect_opts; + git_vector refs; + unsigned connected : 1, + have_refs : 1; +} transport_local; + +static void free_head(git_remote_head *head) +{ + git__free(head->name); + git__free(head->symref_target); + git__free(head); +} + +static void free_heads(git_vector *heads) +{ + git_remote_head *head; + size_t i; + + git_vector_foreach(heads, i, head) + free_head(head); + + git_vector_free(heads); +} + +static int add_ref(transport_local *t, const char *name) +{ + const char peeled[] = "^{}"; + git_reference *ref, *resolved; + git_remote_head *head; + git_oid obj_id; + git_object *obj = NULL, *target = NULL; + git_str buf = GIT_STR_INIT; + int error; + + if ((error = git_reference_lookup(&ref, t->repo, name)) < 0) + return error; + + error = git_reference_resolve(&resolved, ref); + if (error < 0) { + git_reference_free(ref); + if (!strcmp(name, GIT_HEAD_FILE) && error == GIT_ENOTFOUND) { + /* This is actually okay. Empty repos often have a HEAD that + * points to a nonexistent "refs/heads/master". */ + git_error_clear(); + return 0; + } + return error; + } + + git_oid_cpy(&obj_id, git_reference_target(resolved)); + git_reference_free(resolved); + + head = git__calloc(1, sizeof(git_remote_head)); + GIT_ERROR_CHECK_ALLOC(head); + + head->name = git__strdup(name); + GIT_ERROR_CHECK_ALLOC(head->name); + + git_oid_cpy(&head->oid, &obj_id); + + if (git_reference_type(ref) == GIT_REFERENCE_SYMBOLIC) { + head->symref_target = git__strdup(git_reference_symbolic_target(ref)); + GIT_ERROR_CHECK_ALLOC(head->symref_target); + } + git_reference_free(ref); + + if ((error = git_vector_insert(&t->refs, head)) < 0) { + free_head(head); + return error; + } + + /* If it's not a tag, we don't need to try to peel it */ + if (git__prefixcmp(name, GIT_REFS_TAGS_DIR)) + return 0; + + if ((error = git_object_lookup(&obj, t->repo, &head->oid, GIT_OBJECT_ANY)) < 0) + return error; + + head = NULL; + + /* If it's not an annotated tag, or if we're mocking + * git-receive-pack, just get out */ + if (git_object_type(obj) != GIT_OBJECT_TAG || + t->direction != GIT_DIRECTION_FETCH) { + git_object_free(obj); + return 0; + } + + /* And if it's a tag, peel it, and add it to the list */ + head = git__calloc(1, sizeof(git_remote_head)); + GIT_ERROR_CHECK_ALLOC(head); + + if (git_str_join(&buf, 0, name, peeled) < 0) { + free_head(head); + return -1; + } + head->name = git_str_detach(&buf); + + if (!(error = git_tag_peel(&target, (git_tag *)obj))) { + git_oid_cpy(&head->oid, git_object_id(target)); + + if ((error = git_vector_insert(&t->refs, head)) < 0) { + free_head(head); + } + } + + git_object_free(obj); + git_object_free(target); + + return error; +} + +static int store_refs(transport_local *t) +{ + size_t i; + git_remote_head *head; + git_strarray ref_names = {0}; + + GIT_ASSERT_ARG(t); + + if (git_reference_list(&ref_names, t->repo) < 0) + goto on_error; + + /* Clear all heads we might have fetched in a previous connect */ + git_vector_foreach(&t->refs, i, head) { + git__free(head->name); + git__free(head); + } + + /* Clear the vector so we can reuse it */ + git_vector_clear(&t->refs); + + /* Sort the references first */ + git__tsort((void **)ref_names.strings, ref_names.count, &git__strcmp_cb); + + /* Add HEAD iff direction is fetch */ + if (t->direction == GIT_DIRECTION_FETCH && add_ref(t, GIT_HEAD_FILE) < 0) + goto on_error; + + for (i = 0; i < ref_names.count; ++i) { + if (add_ref(t, ref_names.strings[i]) < 0) + goto on_error; + } + + t->have_refs = 1; + git_strarray_dispose(&ref_names); + return 0; + +on_error: + git_vector_free(&t->refs); + git_strarray_dispose(&ref_names); + return -1; +} + +/* + * Try to open the url as a git directory. The direction doesn't + * matter in this case because we're calculating the heads ourselves. + */ +static int local_connect( + git_transport *transport, + const char *url, + int direction, + const git_remote_connect_options *connect_opts) +{ + git_repository *repo; + int error; + transport_local *t = (transport_local *)transport; + const char *path; + git_str buf = GIT_STR_INIT; + + if (t->connected) + return 0; + + if (git_remote_connect_options_normalize(&t->connect_opts, t->owner->repo, connect_opts) < 0) + return -1; + + free_heads(&t->refs); + + t->url = git__strdup(url); + GIT_ERROR_CHECK_ALLOC(t->url); + t->direction = direction; + + /* 'url' may be a url or path; convert to a path */ + if ((error = git_fs_path_from_url_or_path(&buf, url)) < 0) { + git_str_dispose(&buf); + return error; + } + path = git_str_cstr(&buf); + + error = git_repository_open(&repo, path); + + git_str_dispose(&buf); + + if (error < 0) + return -1; + + t->repo = repo; + + if (store_refs(t) < 0) + return -1; + + t->connected = 1; + + return 0; +} + +static int local_set_connect_opts( + git_transport *transport, + const git_remote_connect_options *connect_opts) +{ + transport_local *t = (transport_local *)transport; + + if (!t->connected) { + git_error_set(GIT_ERROR_NET, "cannot reconfigure a transport that is not connected"); + return -1; + } + + return git_remote_connect_options_normalize(&t->connect_opts, t->owner->repo, connect_opts); +} + +static int local_capabilities(unsigned int *capabilities, git_transport *transport) +{ + GIT_UNUSED(transport); + + *capabilities = GIT_REMOTE_CAPABILITY_TIP_OID | + GIT_REMOTE_CAPABILITY_REACHABLE_OID; + return 0; +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +static int local_oid_type(git_oid_t *out, git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + *out = t->repo->oid_type; + + return 0; +} +#endif + +static int local_ls(const git_remote_head ***out, size_t *size, git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + if (!t->have_refs) { + git_error_set(GIT_ERROR_NET, "the transport has not yet loaded the refs"); + return -1; + } + + *out = (const git_remote_head **)t->refs.contents; + *size = t->refs.length; + + return 0; +} + +static int local_negotiate_fetch( + git_transport *transport, + git_repository *repo, + const git_fetch_negotiation *wants) +{ + transport_local *t = (transport_local*)transport; + git_remote_head *rhead; + unsigned int i; + + GIT_UNUSED(wants); + + /* Fill in the loids */ + git_vector_foreach(&t->refs, i, rhead) { + git_object *obj; + + int error = git_revparse_single(&obj, repo, rhead->name); + if (!error) + git_oid_cpy(&rhead->loid, git_object_id(obj)); + else if (error != GIT_ENOTFOUND) + return error; + else + git_error_clear(); + git_object_free(obj); + } + + return 0; +} + +static int local_shallow_roots( + git_oidarray *out, + git_transport *transport) +{ + GIT_UNUSED(out); + GIT_UNUSED(transport); + + return 0; +} + +static int local_push_update_remote_ref( + git_repository *remote_repo, + const char *lref, + const char *rref, + git_oid *loid, + git_oid *roid) +{ + int error; + git_reference *remote_ref = NULL; + + /* check for lhs, if it's empty it means to delete */ + if (lref[0] != '\0') { + /* Create or update a ref */ + error = git_reference_create(NULL, remote_repo, rref, loid, + !git_oid_is_zero(roid), NULL); + } else { + /* Delete a ref */ + if ((error = git_reference_lookup(&remote_ref, remote_repo, rref)) < 0) { + if (error == GIT_ENOTFOUND) + error = 0; + return error; + } + + error = git_reference_delete(remote_ref); + git_reference_free(remote_ref); + } + + return error; +} + +static int transfer_to_push_transfer(const git_indexer_progress *stats, void *payload) +{ + const git_remote_callbacks *cbs = payload; + + if (!cbs || !cbs->push_transfer_progress) + return 0; + + return cbs->push_transfer_progress(stats->received_objects, stats->total_objects, stats->received_bytes, + cbs->payload); +} + +static int local_push( + git_transport *transport, + git_push *push) +{ + transport_local *t = (transport_local *)transport; + git_remote_callbacks *cbs = &t->connect_opts.callbacks; + git_repository *remote_repo = NULL; + push_spec *spec; + char *url = NULL; + const char *path; + git_str buf = GIT_STR_INIT, odb_path = GIT_STR_INIT; + int error; + size_t j; + + /* 'push->remote->url' may be a url or path; convert to a path */ + if ((error = git_fs_path_from_url_or_path(&buf, push->remote->url)) < 0) { + git_str_dispose(&buf); + return error; + } + path = git_str_cstr(&buf); + + error = git_repository_open(&remote_repo, path); + + git_str_dispose(&buf); + + if (error < 0) + return error; + + /* We don't currently support pushing locally to non-bare repos. Proper + non-bare repo push support would require checking configs to see if + we should override the default 'don't let this happen' behavior. + + Note that this is only an issue when pushing to the current branch, + but we forbid all pushes just in case */ + if (!remote_repo->is_bare) { + error = GIT_EBAREREPO; + git_error_set(GIT_ERROR_INVALID, "local push doesn't (yet) support pushing to non-bare repos."); + goto on_error; + } + + if ((error = git_repository__item_path(&odb_path, remote_repo, GIT_REPOSITORY_ITEM_OBJECTS)) < 0 + || (error = git_str_joinpath(&odb_path, odb_path.ptr, "pack")) < 0) + goto on_error; + + error = git_packbuilder_write(push->pb, odb_path.ptr, 0, transfer_to_push_transfer, (void *) cbs); + git_str_dispose(&odb_path); + + if (error < 0) + goto on_error; + + push->unpack_ok = 1; + + git_vector_foreach(&push->specs, j, spec) { + push_status *status; + const git_error *last; + char *ref = spec->refspec.dst; + + status = git__calloc(1, sizeof(push_status)); + if (!status) + goto on_error; + + status->ref = git__strdup(ref); + if (!status->ref) { + git_push_status_free(status); + goto on_error; + } + + error = local_push_update_remote_ref(remote_repo, spec->refspec.src, spec->refspec.dst, + &spec->loid, &spec->roid); + + switch (error) { + case GIT_OK: + break; + case GIT_EINVALIDSPEC: + status->msg = git__strdup("funny refname"); + break; + case GIT_ENOTFOUND: + status->msg = git__strdup("Remote branch not found to delete"); + break; + default: + last = git_error_last(); + + if (last && last->message) + status->msg = git__strdup(last->message); + else + status->msg = git__strdup("Unspecified error encountered"); + break; + } + + /* failed to allocate memory for a status message */ + if (error < 0 && !status->msg) { + git_push_status_free(status); + goto on_error; + } + + /* failed to insert the ref update status */ + if ((error = git_vector_insert(&push->status, status)) < 0) { + git_push_status_free(status); + goto on_error; + } + } + + if (push->specs.length) { + url = git__strdup(t->url); + + if (!url || t->parent.close(&t->parent) < 0 || + t->parent.connect(&t->parent, url, + GIT_DIRECTION_PUSH, NULL)) + goto on_error; + } + + error = 0; + +on_error: + git_repository_free(remote_repo); + git__free(url); + + return error; +} + +typedef struct foreach_data { + git_indexer_progress *stats; + git_indexer_progress_cb progress_cb; + void *progress_payload; + git_odb_writepack *writepack; +} foreach_data; + +static int foreach_cb(void *buf, size_t len, void *payload) +{ + foreach_data *data = (foreach_data*)payload; + + data->stats->received_bytes += len; + return data->writepack->append(data->writepack, buf, len, data->stats); +} + +static const char *counting_objects_fmt = "Counting objects %d\r"; +static const char *compressing_objects_fmt = "Compressing objects: %.0f%% (%d/%d)"; + +static int local_counting(int stage, unsigned int current, unsigned int total, void *payload) +{ + git_str progress_info = GIT_STR_INIT; + transport_local *t = payload; + int error; + + if (!t->connect_opts.callbacks.sideband_progress) + return 0; + + if (stage == GIT_PACKBUILDER_ADDING_OBJECTS) { + git_str_printf(&progress_info, counting_objects_fmt, current); + } else if (stage == GIT_PACKBUILDER_DELTAFICATION) { + float perc = (((float) current) / total) * 100; + git_str_printf(&progress_info, compressing_objects_fmt, perc, current, total); + if (current == total) + git_str_printf(&progress_info, ", done\n"); + else + git_str_putc(&progress_info, '\r'); + + } + + if (git_str_oom(&progress_info)) + return -1; + + if (progress_info.size > INT_MAX) { + git_error_set(GIT_ERROR_NET, "remote sent overly large progress data"); + git_str_dispose(&progress_info); + return -1; + } + + + error = t->connect_opts.callbacks.sideband_progress( + progress_info.ptr, + (int)progress_info.size, + t->connect_opts.callbacks.payload); + + git_str_dispose(&progress_info); + return error; +} + +static int foreach_reference_cb(git_reference *reference, void *payload) +{ + git_revwalk *walk = (git_revwalk *)payload; + int error; + + if (git_reference_type(reference) != GIT_REFERENCE_DIRECT) { + git_reference_free(reference); + return 0; + } + + error = git_revwalk_hide(walk, git_reference_target(reference)); + /* The reference is in the local repository, so the target may not + * exist on the remote. It also may not be a commit. */ + if (error == GIT_ENOTFOUND || error == GIT_ERROR_INVALID) { + git_error_clear(); + error = 0; + } + + git_reference_free(reference); + + return error; +} + +static int local_download_pack( + git_transport *transport, + git_repository *repo, + git_indexer_progress *stats) +{ + transport_local *t = (transport_local*)transport; + git_revwalk *walk = NULL; + git_remote_head *rhead; + unsigned int i; + int error = -1; + git_packbuilder *pack = NULL; + git_odb_writepack *writepack = NULL; + git_odb *odb = NULL; + git_str progress_info = GIT_STR_INIT; + foreach_data data = {0}; + + if ((error = git_revwalk_new(&walk, t->repo)) < 0) + goto cleanup; + + git_revwalk_sorting(walk, GIT_SORT_TIME); + + if ((error = git_packbuilder_new(&pack, t->repo)) < 0) + goto cleanup; + + git_packbuilder_set_callbacks(pack, local_counting, t); + + stats->total_objects = 0; + stats->indexed_objects = 0; + stats->received_objects = 0; + stats->received_bytes = 0; + + git_vector_foreach(&t->refs, i, rhead) { + git_object *obj; + if ((error = git_object_lookup(&obj, t->repo, &rhead->oid, GIT_OBJECT_ANY)) < 0) + goto cleanup; + + if (git_object_type(obj) == GIT_OBJECT_COMMIT) { + /* Revwalker includes only wanted commits */ + error = git_revwalk_push(walk, &rhead->oid); + } else { + /* Tag or some other wanted object. Add it on its own */ + error = git_packbuilder_insert_recur(pack, &rhead->oid, rhead->name); + } + git_object_free(obj); + if (error < 0) + goto cleanup; + } + + if ((error = git_reference_foreach(repo, foreach_reference_cb, walk))) + goto cleanup; + + if ((error = git_packbuilder_insert_walk(pack, walk))) + goto cleanup; + + if (t->connect_opts.callbacks.sideband_progress) { + if ((error = git_str_printf( + &progress_info, + counting_objects_fmt, + git_packbuilder_object_count(pack))) < 0 || + (error = t->connect_opts.callbacks.sideband_progress( + progress_info.ptr, + (int)progress_info.size, + t->connect_opts.callbacks.payload)) < 0) + goto cleanup; + } + + /* Walk the objects, building a packfile */ + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0) + goto cleanup; + + /* One last one with the newline */ + if (t->connect_opts.callbacks.sideband_progress) { + git_str_clear(&progress_info); + + if ((error = git_str_printf( + &progress_info, + counting_objects_fmt, + git_packbuilder_object_count(pack))) < 0 || + (error = git_str_putc(&progress_info, '\n')) < 0 || + (error = t->connect_opts.callbacks.sideband_progress( + progress_info.ptr, + (int)progress_info.size, + t->connect_opts.callbacks.payload)) < 0) + goto cleanup; + } + + if ((error = git_odb_write_pack( + &writepack, + odb, + t->connect_opts.callbacks.transfer_progress, + t->connect_opts.callbacks.payload)) < 0) + goto cleanup; + + /* Write the data to the ODB */ + data.stats = stats; + data.progress_cb = t->connect_opts.callbacks.transfer_progress; + data.progress_payload = t->connect_opts.callbacks.payload; + data.writepack = writepack; + + /* autodetect */ + git_packbuilder_set_threads(pack, 0); + + if ((error = git_packbuilder_foreach(pack, foreach_cb, &data)) != 0) + goto cleanup; + + error = writepack->commit(writepack, stats); + +cleanup: + if (writepack) writepack->free(writepack); + git_str_dispose(&progress_info); + git_packbuilder_free(pack); + git_revwalk_free(walk); + return error; +} + +static int local_is_connected(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + return t->connected; +} + +static void local_cancel(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + git_atomic32_set(&t->cancelled, 1); +} + +static int local_close(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + t->connected = 0; + + if (t->repo) { + git_repository_free(t->repo); + t->repo = NULL; + } + + if (t->url) { + git__free(t->url); + t->url = NULL; + } + + return 0; +} + +static void local_free(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + free_heads(&t->refs); + + /* Close the transport, if it's still open. */ + local_close(transport); + + /* Free the transport */ + git__free(t); +} + +/************** + * Public API * + **************/ + +int git_transport_local(git_transport **out, git_remote *owner, void *param) +{ + int error; + transport_local *t; + + GIT_UNUSED(param); + + t = git__calloc(1, sizeof(transport_local)); + GIT_ERROR_CHECK_ALLOC(t); + + t->parent.version = GIT_TRANSPORT_VERSION; + t->parent.connect = local_connect; + t->parent.set_connect_opts = local_set_connect_opts; + t->parent.capabilities = local_capabilities; +#ifdef GIT_EXPERIMENTAL_SHA256 + t->parent.oid_type = local_oid_type; +#endif + t->parent.negotiate_fetch = local_negotiate_fetch; + t->parent.shallow_roots = local_shallow_roots; + t->parent.download_pack = local_download_pack; + t->parent.push = local_push; + t->parent.close = local_close; + t->parent.free = local_free; + t->parent.ls = local_ls; + t->parent.is_connected = local_is_connected; + t->parent.cancel = local_cancel; + + if ((error = git_vector_init(&t->refs, 0, NULL)) < 0) { + git__free(t); + return error; + } + + t->owner = owner; + + *out = (git_transport *) t; + + return 0; +} diff --git a/src/libgit2/transports/smart.c b/src/libgit2/transports/smart.c new file mode 100644 index 0000000..5372728 --- /dev/null +++ b/src/libgit2/transports/smart.c @@ -0,0 +1,525 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "smart.h" + +#include "git2.h" +#include "git2/sys/remote.h" +#include "refs.h" +#include "refspec.h" +#include "proxy.h" + +int git_smart__recv(transport_smart *t) +{ + size_t bytes_read; + int ret; + + GIT_ASSERT_ARG(t); + GIT_ASSERT(t->current_stream); + + if (git_staticstr_remain(&t->buffer) == 0) { + git_error_set(GIT_ERROR_NET, "out of buffer space"); + return -1; + } + + ret = t->current_stream->read(t->current_stream, + git_staticstr_offset(&t->buffer), + git_staticstr_remain(&t->buffer), + &bytes_read); + + if (ret < 0) + return ret; + + GIT_ASSERT(bytes_read <= INT_MAX); + GIT_ASSERT(bytes_read <= git_staticstr_remain(&t->buffer)); + + git_staticstr_increase(&t->buffer, bytes_read); + + if (t->packetsize_cb && !t->cancelled.val) { + ret = t->packetsize_cb(bytes_read, t->packetsize_payload); + + if (ret) { + git_atomic32_set(&t->cancelled, 1); + return GIT_EUSER; + } + } + + return (int)bytes_read; +} + +GIT_INLINE(int) git_smart__reset_stream(transport_smart *t, bool close_subtransport) +{ + if (t->current_stream) { + t->current_stream->free(t->current_stream); + t->current_stream = NULL; + } + + if (close_subtransport) { + git__free(t->url); + t->url = NULL; + + if (t->wrapped->close(t->wrapped) < 0) + return -1; + } + + git__free(t->caps.object_format); + t->caps.object_format = NULL; + + git__free(t->caps.agent); + t->caps.agent = NULL; + + return 0; +} + +int git_smart__update_heads(transport_smart *t, git_vector *symrefs) +{ + size_t i; + git_pkt *pkt; + + git_vector_clear(&t->heads); + git_vector_foreach(&t->refs, i, pkt) { + git_pkt_ref *ref = (git_pkt_ref *) pkt; + if (pkt->type != GIT_PKT_REF) + continue; + + if (symrefs) { + git_refspec *spec; + git_str buf = GIT_STR_INIT; + size_t j; + int error = 0; + + git_vector_foreach(symrefs, j, spec) { + git_str_clear(&buf); + if (git_refspec_src_matches(spec, ref->head.name) && + !(error = git_refspec__transform(&buf, spec, ref->head.name))) { + git__free(ref->head.symref_target); + ref->head.symref_target = git_str_detach(&buf); + } + } + + git_str_dispose(&buf); + + if (error < 0) + return error; + } + + if (git_vector_insert(&t->heads, &ref->head) < 0) + return -1; + } + + return 0; +} + +static void free_symrefs(git_vector *symrefs) +{ + git_refspec *spec; + size_t i; + + git_vector_foreach(symrefs, i, spec) { + git_refspec__dispose(spec); + git__free(spec); + } + + git_vector_free(symrefs); +} + +static int git_smart__connect( + git_transport *transport, + const char *url, + int direction, + const git_remote_connect_options *connect_opts) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + git_smart_subtransport_stream *stream; + int error; + git_pkt *pkt; + git_pkt_ref *first; + git_vector symrefs; + git_smart_service_t service; + + if (git_smart__reset_stream(t, true) < 0) + return -1; + + if (git_remote_connect_options_normalize(&t->connect_opts, t->owner->repo, connect_opts) < 0) + return -1; + + t->url = git__strdup(url); + GIT_ERROR_CHECK_ALLOC(t->url); + + t->direction = direction; + + if (GIT_DIRECTION_FETCH == t->direction) { + service = GIT_SERVICE_UPLOADPACK_LS; + } else if (GIT_DIRECTION_PUSH == t->direction) { + service = GIT_SERVICE_RECEIVEPACK_LS; + } else { + git_error_set(GIT_ERROR_NET, "invalid direction"); + return -1; + } + + if ((error = t->wrapped->action(&stream, t->wrapped, t->url, service)) < 0) + return error; + + /* Save off the current stream (i.e. socket) that we are working with */ + t->current_stream = stream; + + /* 2 flushes for RPC; 1 for stateful */ + if ((error = git_smart__store_refs(t, t->rpc ? 2 : 1)) < 0) + return error; + + /* Strip the comment packet for RPC */ + if (t->rpc) { + pkt = (git_pkt *)git_vector_get(&t->refs, 0); + + if (!pkt || GIT_PKT_COMMENT != pkt->type) { + git_error_set(GIT_ERROR_NET, "invalid response"); + return -1; + } else { + /* Remove the comment pkt from the list */ + git_vector_remove(&t->refs, 0); + git__free(pkt); + } + } + + /* We now have loaded the refs. */ + t->have_refs = 1; + + pkt = (git_pkt *)git_vector_get(&t->refs, 0); + if (pkt && GIT_PKT_REF != pkt->type) { + git_error_set(GIT_ERROR_NET, "invalid response"); + return -1; + } + first = (git_pkt_ref *)pkt; + + if ((error = git_vector_init(&symrefs, 1, NULL)) < 0) + return error; + + /* Detect capabilities */ + if ((error = git_smart__detect_caps(first, &t->caps, &symrefs)) == 0) { + /* If the only ref in the list is capabilities^{} with OID_ZERO, remove it */ + if (1 == t->refs.length && !strcmp(first->head.name, "capabilities^{}") && + git_oid_is_zero(&first->head.oid)) { + git_vector_clear(&t->refs); + git_pkt_free((git_pkt *)first); + } + + /* Keep a list of heads for _ls */ + git_smart__update_heads(t, &symrefs); + } else if (error == GIT_ENOTFOUND) { + /* There was no ref packet received, or the cap list was empty */ + error = 0; + } else { + git_error_set(GIT_ERROR_NET, "invalid response"); + goto cleanup; + } + + if (t->rpc && (error = git_smart__reset_stream(t, false)) < 0) + goto cleanup; + + /* We're now logically connected. */ + t->connected = 1; + +cleanup: + free_symrefs(&symrefs); + + return error; +} + +static int git_smart__set_connect_opts( + git_transport *transport, + const git_remote_connect_options *opts) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + + if (!t->connected) { + git_error_set(GIT_ERROR_NET, "cannot reconfigure a transport that is not connected"); + return -1; + } + + return git_remote_connect_options_normalize(&t->connect_opts, t->owner->repo, opts); +} + +static int git_smart__capabilities(unsigned int *capabilities, git_transport *transport) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + + *capabilities = 0; + + if (t->caps.want_tip_sha1) + *capabilities |= GIT_REMOTE_CAPABILITY_TIP_OID; + + if (t->caps.want_reachable_sha1) + *capabilities |= GIT_REMOTE_CAPABILITY_REACHABLE_OID; + + return 0; +} + +#ifdef GIT_EXPERIMENTAL_SHA256 +static int git_smart__oid_type(git_oid_t *out, git_transport *transport) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + + *out = 0; + + if (t->caps.object_format == NULL) { + *out = GIT_OID_DEFAULT; + } else { + *out = git_oid_type_fromstr(t->caps.object_format); + + if (!*out) { + git_error_set(GIT_ERROR_INVALID, + "unknown object format '%s'", + t->caps.object_format); + return -1; + } + } + + return 0; +} +#endif + +static int git_smart__ls(const git_remote_head ***out, size_t *size, git_transport *transport) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + + if (!t->have_refs) { + git_error_set(GIT_ERROR_NET, "the transport has not yet loaded the refs"); + return -1; + } + + *out = (const git_remote_head **) t->heads.contents; + *size = t->heads.length; + + return 0; +} + +int git_smart__negotiation_step(git_transport *transport, void *data, size_t len) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + git_smart_subtransport_stream *stream; + int error; + + if (t->rpc && git_smart__reset_stream(t, false) < 0) + return -1; + + if (GIT_DIRECTION_FETCH != t->direction) { + git_error_set(GIT_ERROR_NET, "this operation is only valid for fetch"); + return -1; + } + + if ((error = t->wrapped->action(&stream, t->wrapped, t->url, GIT_SERVICE_UPLOADPACK)) < 0) + return error; + + /* If this is a stateful implementation, the stream we get back should be the same */ + GIT_ASSERT(t->rpc || t->current_stream == stream); + + /* Save off the current stream (i.e. socket) that we are working with */ + t->current_stream = stream; + + if ((error = stream->write(stream, (const char *)data, len)) < 0) + return error; + + return 0; +} + +int git_smart__get_push_stream(transport_smart *t, git_smart_subtransport_stream **stream) +{ + int error; + + if (t->rpc && git_smart__reset_stream(t, false) < 0) + return -1; + + if (GIT_DIRECTION_PUSH != t->direction) { + git_error_set(GIT_ERROR_NET, "this operation is only valid for push"); + return -1; + } + + if ((error = t->wrapped->action(stream, t->wrapped, t->url, GIT_SERVICE_RECEIVEPACK)) < 0) + return error; + + /* If this is a stateful implementation, the stream we get back should be the same */ + GIT_ASSERT(t->rpc || t->current_stream == *stream); + + /* Save off the current stream (i.e. socket) that we are working with */ + t->current_stream = *stream; + + return 0; +} + +static void git_smart__cancel(git_transport *transport) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + + git_atomic32_set(&t->cancelled, 1); +} + +static int git_smart__is_connected(git_transport *transport) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + + return t->connected; +} + +static int git_smart__close(git_transport *transport) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + git_vector *common = &t->common; + unsigned int i; + git_pkt *p; + int ret; + git_smart_subtransport_stream *stream; + const char flush[] = "0000"; + + /* + * If we're still connected at this point and not using RPC, + * we should say goodbye by sending a flush, or git-daemon + * will complain that we disconnected unexpectedly. + */ + if (t->connected && !t->rpc && + !t->wrapped->action(&stream, t->wrapped, t->url, GIT_SERVICE_UPLOADPACK)) { + t->current_stream->write(t->current_stream, flush, 4); + } + + ret = git_smart__reset_stream(t, true); + + git_vector_foreach(common, i, p) + git_pkt_free(p); + + git_vector_free(common); + + if (t->url) { + git__free(t->url); + t->url = NULL; + } + + t->connected = 0; + + return ret; +} + +static void git_smart__free(git_transport *transport) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + git_vector *refs = &t->refs; + unsigned int i; + git_pkt *p; + + /* Make sure that the current stream is closed, if we have one. */ + git_smart__close(transport); + + /* Free the subtransport */ + t->wrapped->free(t->wrapped); + + git_vector_free(&t->heads); + git_vector_foreach(refs, i, p) + git_pkt_free(p); + + git_vector_free(refs); + + git_remote_connect_options_dispose(&t->connect_opts); + + git_array_dispose(t->shallow_roots); + + git__free(t->caps.object_format); + git__free(t->caps.agent); + git__free(t); +} + +static int ref_name_cmp(const void *a, const void *b) +{ + const git_pkt_ref *ref_a = a, *ref_b = b; + + return strcmp(ref_a->head.name, ref_b->head.name); +} + +int git_transport_smart_certificate_check(git_transport *transport, git_cert *cert, int valid, const char *hostname) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + git_remote_connect_options *connect_opts = &t->connect_opts; + + GIT_ASSERT_ARG(transport); + GIT_ASSERT_ARG(cert); + GIT_ASSERT_ARG(hostname); + + if (!connect_opts->callbacks.certificate_check) + return GIT_PASSTHROUGH; + + return connect_opts->callbacks.certificate_check(cert, valid, hostname, connect_opts->callbacks.payload); +} + +int git_transport_smart_credentials(git_credential **out, git_transport *transport, const char *user, int methods) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + git_remote_connect_options *connect_opts = &t->connect_opts; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(transport); + + if (!connect_opts->callbacks.credentials) + return GIT_PASSTHROUGH; + + return connect_opts->callbacks.credentials(out, t->url, user, methods, connect_opts->callbacks.payload); +} + +int git_transport_remote_connect_options( + git_remote_connect_options *out, + git_transport *transport) +{ + transport_smart *t = GIT_CONTAINER_OF(transport, transport_smart, parent); + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(transport); + + return git_remote_connect_options_dup(out, &t->connect_opts); +} + +int git_transport_smart(git_transport **out, git_remote *owner, void *param) +{ + transport_smart *t; + git_smart_subtransport_definition *definition = (git_smart_subtransport_definition *)param; + + if (!param) + return -1; + + t = git__calloc(1, sizeof(transport_smart)); + GIT_ERROR_CHECK_ALLOC(t); + + t->parent.version = GIT_TRANSPORT_VERSION; + t->parent.connect = git_smart__connect; + t->parent.set_connect_opts = git_smart__set_connect_opts; + t->parent.capabilities = git_smart__capabilities; +#ifdef GIT_EXPERIMENTAL_SHA256 + t->parent.oid_type = git_smart__oid_type; +#endif + t->parent.close = git_smart__close; + t->parent.free = git_smart__free; + t->parent.negotiate_fetch = git_smart__negotiate_fetch; + t->parent.shallow_roots = git_smart__shallow_roots; + t->parent.download_pack = git_smart__download_pack; + t->parent.push = git_smart__push; + t->parent.ls = git_smart__ls; + t->parent.is_connected = git_smart__is_connected; + t->parent.cancel = git_smart__cancel; + + t->owner = owner; + t->rpc = definition->rpc; + + if (git_vector_init(&t->refs, 16, ref_name_cmp) < 0 || + git_vector_init(&t->heads, 16, ref_name_cmp) < 0 || + definition->callback(&t->wrapped, &t->parent, definition->param) < 0) { + git_vector_free(&t->refs); + git_vector_free(&t->heads); + t->wrapped->free(t->wrapped); + git__free(t); + return -1; + } + + git_staticstr_init(&t->buffer, GIT_SMART_BUFFER_SIZE); + + *out = (git_transport *) t; + return 0; +} diff --git a/src/libgit2/transports/smart.h b/src/libgit2/transports/smart.h new file mode 100644 index 0000000..52c7553 --- /dev/null +++ b/src/libgit2/transports/smart.h @@ -0,0 +1,217 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_transports_smart_h__ +#define INCLUDE_transports_smart_h__ + +#include "common.h" + +#include "git2.h" +#include "vector.h" +#include "push.h" +#include "str.h" +#include "oidarray.h" +#include "staticstr.h" +#include "git2/sys/transport.h" + +#define GIT_SMART_BUFFER_SIZE 65536 + +#define GIT_SIDE_BAND_DATA 1 +#define GIT_SIDE_BAND_PROGRESS 2 +#define GIT_SIDE_BAND_ERROR 3 + +#define GIT_CAP_OFS_DELTA "ofs-delta" +#define GIT_CAP_MULTI_ACK "multi_ack" +#define GIT_CAP_MULTI_ACK_DETAILED "multi_ack_detailed" +#define GIT_CAP_SIDE_BAND "side-band" +#define GIT_CAP_SIDE_BAND_64K "side-band-64k" +#define GIT_CAP_INCLUDE_TAG "include-tag" +#define GIT_CAP_DELETE_REFS "delete-refs" +#define GIT_CAP_REPORT_STATUS "report-status" +#define GIT_CAP_THIN_PACK "thin-pack" +#define GIT_CAP_SYMREF "symref" +#define GIT_CAP_WANT_TIP_SHA1 "allow-tip-sha1-in-want" +#define GIT_CAP_WANT_REACHABLE_SHA1 "allow-reachable-sha1-in-want" +#define GIT_CAP_SHALLOW "shallow" +#define GIT_CAP_OBJECT_FORMAT "object-format=" +#define GIT_CAP_AGENT "agent=" + +extern bool git_smart__ofs_delta_enabled; + +typedef enum { + GIT_PKT_CMD, + GIT_PKT_FLUSH, + GIT_PKT_REF, + GIT_PKT_HAVE, + GIT_PKT_ACK, + GIT_PKT_NAK, + GIT_PKT_COMMENT, + GIT_PKT_ERR, + GIT_PKT_DATA, + GIT_PKT_PROGRESS, + GIT_PKT_OK, + GIT_PKT_NG, + GIT_PKT_UNPACK, + GIT_PKT_SHALLOW, + GIT_PKT_UNSHALLOW +} git_pkt_type; + +/* Used for multi_ack and multi_ack_detailed */ +enum git_ack_status { + GIT_ACK_NONE, + GIT_ACK_CONTINUE, + GIT_ACK_COMMON, + GIT_ACK_READY +}; + +/* This would be a flush pkt */ +typedef struct { + git_pkt_type type; +} git_pkt; + +struct git_pkt_cmd { + git_pkt_type type; + char *cmd; + char *path; + char *host; +}; + +/* This is a pkt-line with some info in it */ +typedef struct { + git_pkt_type type; + git_remote_head head; + char *capabilities; +} git_pkt_ref; + +/* Useful later */ +typedef struct { + git_pkt_type type; + git_oid oid; + enum git_ack_status status; +} git_pkt_ack; + +typedef struct { + git_pkt_type type; + char comment[GIT_FLEX_ARRAY]; +} git_pkt_comment; + +typedef struct { + git_pkt_type type; + size_t len; + char data[GIT_FLEX_ARRAY]; +} git_pkt_data; + +typedef git_pkt_data git_pkt_progress; + +typedef struct { + git_pkt_type type; + size_t len; + char error[GIT_FLEX_ARRAY]; +} git_pkt_err; + +typedef struct { + git_pkt_type type; + char *ref; +} git_pkt_ok; + +typedef struct { + git_pkt_type type; + char *ref; + char *msg; +} git_pkt_ng; + +typedef struct { + git_pkt_type type; + int unpack_ok; +} git_pkt_unpack; + +typedef struct { + git_pkt_type type; + git_oid oid; +} git_pkt_shallow; + +typedef struct transport_smart_caps { + unsigned int common:1, + ofs_delta:1, + multi_ack:1, + multi_ack_detailed:1, + side_band:1, + side_band_64k:1, + include_tag:1, + delete_refs:1, + report_status:1, + thin_pack:1, + want_tip_sha1:1, + want_reachable_sha1:1, + shallow:1; + char *object_format; + char *agent; +} transport_smart_caps; + +typedef int (*packetsize_cb)(size_t received, void *payload); + +typedef struct { + git_transport parent; + git_remote *owner; + char *url; + git_remote_connect_options connect_opts; + int direction; + git_smart_subtransport *wrapped; + git_smart_subtransport_stream *current_stream; + transport_smart_caps caps; + git_vector refs; + git_vector heads; + git_vector common; + git_array_oid_t shallow_roots; + git_atomic32 cancelled; + packetsize_cb packetsize_cb; + void *packetsize_payload; + unsigned rpc : 1, + have_refs : 1, + connected : 1; + git_staticstr_with_size(GIT_SMART_BUFFER_SIZE) buffer; +} transport_smart; + +/* smart_protocol.c */ +int git_smart__store_refs(transport_smart *t, int flushes); +int git_smart__detect_caps(git_pkt_ref *pkt, transport_smart_caps *caps, git_vector *symrefs); +int git_smart__push(git_transport *transport, git_push *push); + +int git_smart__negotiate_fetch( + git_transport *transport, + git_repository *repo, + const git_fetch_negotiation *wants); + +int git_smart__shallow_roots(git_oidarray *out, git_transport *transport); + +int git_smart__download_pack( + git_transport *transport, + git_repository *repo, + git_indexer_progress *stats); + +/* smart.c */ +int git_smart__recv(transport_smart *t); + +int git_smart__negotiation_step(git_transport *transport, void *data, size_t len); +int git_smart__get_push_stream(transport_smart *t, git_smart_subtransport_stream **out); + +int git_smart__update_heads(transport_smart *t, git_vector *symrefs); + +/* smart_pkt.c */ +typedef struct { + git_oid_t oid_type; + int seen_capabilities: 1; +} git_pkt_parse_data; + +int git_pkt_parse_line(git_pkt **head, const char **endptr, const char *line, size_t linelen, git_pkt_parse_data *data); +int git_pkt_buffer_flush(git_str *buf); +int git_pkt_send_flush(GIT_SOCKET s); +int git_pkt_buffer_done(git_str *buf); +int git_pkt_buffer_wants(const git_fetch_negotiation *wants, transport_smart_caps *caps, git_str *buf); +int git_pkt_buffer_have(git_oid *oid, git_str *buf); +void git_pkt_free(git_pkt *pkt); + +#endif diff --git a/src/libgit2/transports/smart_pkt.c b/src/libgit2/transports/smart_pkt.c new file mode 100644 index 0000000..3307acf --- /dev/null +++ b/src/libgit2/transports/smart_pkt.c @@ -0,0 +1,870 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "smart.h" +#include "util.h" +#include "posix.h" +#include "str.h" +#include "oid.h" + +#include "git2/types.h" +#include "git2/errors.h" +#include "git2/refs.h" +#include "git2/revwalk.h" + +#include + +#define PKT_DONE_STR "0009done\n" +#define PKT_FLUSH_STR "0000" +#define PKT_HAVE_PREFIX "have " +#define PKT_WANT_PREFIX "want " + +#define PKT_LEN_SIZE 4 +#define PKT_MAX_SIZE 0xffff +#define PKT_MAX_WANTLEN (PKT_LEN_SIZE + CONST_STRLEN(PKT_WANT_PREFIX) + GIT_OID_MAX_HEXSIZE + 1) + +static int flush_pkt(git_pkt **out) +{ + git_pkt *pkt; + + pkt = git__malloc(sizeof(git_pkt)); + GIT_ERROR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_FLUSH; + *out = pkt; + + return 0; +} + +/* the rest of the line will be useful for multi_ack and multi_ack_detailed */ +static int ack_pkt( + git_pkt **out, + const char *line, + size_t len, + git_pkt_parse_data *data) +{ + git_pkt_ack *pkt; + size_t oid_hexsize = git_oid_hexsize(data->oid_type); + + GIT_ASSERT(data && data->oid_type); + + pkt = git__calloc(1, sizeof(git_pkt_ack)); + GIT_ERROR_CHECK_ALLOC(pkt); + pkt->type = GIT_PKT_ACK; + + if (git__prefixncmp(line, len, "ACK ")) + goto out_err; + line += 4; + len -= 4; + + if (len < oid_hexsize || + git_oid__fromstr(&pkt->oid, line, data->oid_type) < 0) + goto out_err; + line += oid_hexsize; + len -= oid_hexsize; + + if (len && line[0] == ' ') { + line++; + len--; + + if (!git__prefixncmp(line, len, "continue")) + pkt->status = GIT_ACK_CONTINUE; + else if (!git__prefixncmp(line, len, "common")) + pkt->status = GIT_ACK_COMMON; + else if (!git__prefixncmp(line, len, "ready")) + pkt->status = GIT_ACK_READY; + else + goto out_err; + } + + *out = (git_pkt *) pkt; + + return 0; + +out_err: + git_error_set(GIT_ERROR_NET, "error parsing ACK pkt-line"); + git__free(pkt); + return -1; +} + +static int nak_pkt(git_pkt **out) +{ + git_pkt *pkt; + + pkt = git__malloc(sizeof(git_pkt)); + GIT_ERROR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_NAK; + *out = pkt; + + return 0; +} + +static int comment_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_comment *pkt; + size_t alloclen; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, sizeof(git_pkt_comment), len); + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, 1); + pkt = git__malloc(alloclen); + GIT_ERROR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_COMMENT; + memcpy(pkt->comment, line, len); + pkt->comment[len] = '\0'; + + *out = (git_pkt *) pkt; + + return 0; +} + +static int err_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_err *pkt = NULL; + size_t alloclen; + + /* Remove "ERR " from the line */ + if (git__prefixncmp(line, len, "ERR ")) + goto out_err; + line += 4; + len -= 4; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, sizeof(git_pkt_progress), len); + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, 1); + pkt = git__malloc(alloclen); + GIT_ERROR_CHECK_ALLOC(pkt); + pkt->type = GIT_PKT_ERR; + pkt->len = len; + + memcpy(pkt->error, line, len); + pkt->error[len] = '\0'; + + *out = (git_pkt *) pkt; + + return 0; + +out_err: + git_error_set(GIT_ERROR_NET, "error parsing ERR pkt-line"); + git__free(pkt); + return -1; +} + +static int data_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_data *pkt; + size_t alloclen; + + line++; + len--; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, sizeof(git_pkt_progress), len); + pkt = git__malloc(alloclen); + GIT_ERROR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_DATA; + pkt->len = len; + memcpy(pkt->data, line, len); + + *out = (git_pkt *) pkt; + + return 0; +} + +static int sideband_progress_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_progress *pkt; + size_t alloclen; + + line++; + len--; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, sizeof(git_pkt_progress), len); + pkt = git__malloc(alloclen); + GIT_ERROR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_PROGRESS; + pkt->len = len; + memcpy(pkt->data, line, len); + + *out = (git_pkt *) pkt; + + return 0; +} + +static int sideband_error_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_err *pkt; + size_t alloc_len; + + line++; + len--; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, sizeof(git_pkt_err), len); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, 1); + pkt = git__malloc(alloc_len); + GIT_ERROR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_ERR; + pkt->len = (int)len; + memcpy(pkt->error, line, len); + pkt->error[len] = '\0'; + + *out = (git_pkt *)pkt; + + return 0; +} + +static int set_data( + git_pkt_parse_data *data, + const char *line, + size_t len) +{ + const char *caps, *format_str = NULL, *eos; + size_t format_len; + git_oid_t remote_oid_type; + + GIT_ASSERT_ARG(data); + + if ((caps = memchr(line, '\0', len)) != NULL && + len > (size_t)((caps - line) + 1)) { + caps++; + + if (strncmp(caps, "object-format=", CONST_STRLEN("object-format=")) == 0) + format_str = caps + CONST_STRLEN("object-format="); + else if ((format_str = strstr(caps, " object-format=")) != NULL) + format_str += CONST_STRLEN(" object-format="); + } + + if (format_str) { + if ((eos = strchr(format_str, ' ')) == NULL) + eos = strchr(format_str, '\0'); + + GIT_ASSERT(eos); + + format_len = eos - format_str; + + if ((remote_oid_type = git_oid_type_fromstrn(format_str, format_len)) == 0) { + git_error_set(GIT_ERROR_INVALID, "unknown remote object format '%.*s'", (int)format_len, format_str); + return -1; + } + } else { + remote_oid_type = GIT_OID_SHA1; + } + + if (!data->oid_type) { + data->oid_type = remote_oid_type; + } else if (data->oid_type != remote_oid_type) { + git_error_set(GIT_ERROR_INVALID, + "the local object format '%s' does not match the remote object format '%s'", + git_oid_type_name(data->oid_type), + git_oid_type_name(remote_oid_type)); + return -1; + } + + return 0; +} + +/* + * Parse an other-ref line. + */ +static int ref_pkt( + git_pkt **out, + const char *line, + size_t len, + git_pkt_parse_data *data) +{ + git_pkt_ref *pkt; + size_t alloclen, oid_hexsize; + + pkt = git__calloc(1, sizeof(git_pkt_ref)); + GIT_ERROR_CHECK_ALLOC(pkt); + pkt->type = GIT_PKT_REF; + + /* Determine OID type from capabilities */ + if (!data->seen_capabilities && set_data(data, line, len) < 0) + return -1; + + GIT_ASSERT(data->oid_type); + oid_hexsize = git_oid_hexsize(data->oid_type); + + if (len < oid_hexsize || + git_oid__fromstr(&pkt->head.oid, line, data->oid_type) < 0) + goto out_err; + line += oid_hexsize; + len -= oid_hexsize; + + if (git__prefixncmp(line, len, " ")) + goto out_err; + + line++; + len--; + + if (!len) + goto out_err; + + if (line[len - 1] == '\n') + --len; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, len, 1); + pkt->head.name = git__malloc(alloclen); + GIT_ERROR_CHECK_ALLOC(pkt->head.name); + + memcpy(pkt->head.name, line, len); + pkt->head.name[len] = '\0'; + + if (strlen(pkt->head.name) < len) { + if (!data->seen_capabilities) + pkt->capabilities = strchr(pkt->head.name, '\0') + 1; + else + goto out_err; + } + + data->seen_capabilities = 1; + + *out = (git_pkt *)pkt; + return 0; + +out_err: + git_error_set(GIT_ERROR_NET, "error parsing REF pkt-line"); + if (pkt) + git__free(pkt->head.name); + git__free(pkt); + return -1; +} + +static int ok_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_ok *pkt; + size_t alloc_len; + + pkt = git__malloc(sizeof(*pkt)); + GIT_ERROR_CHECK_ALLOC(pkt); + pkt->type = GIT_PKT_OK; + + if (git__prefixncmp(line, len, "ok ")) + goto out_err; + line += 3; + len -= 3; + + if (len && line[len - 1] == '\n') + --len; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, len, 1); + pkt->ref = git__malloc(alloc_len); + GIT_ERROR_CHECK_ALLOC(pkt->ref); + + memcpy(pkt->ref, line, len); + pkt->ref[len] = '\0'; + + *out = (git_pkt *)pkt; + return 0; + +out_err: + git_error_set(GIT_ERROR_NET, "error parsing OK pkt-line"); + git__free(pkt); + return -1; +} + +static int ng_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_ng *pkt; + const char *ptr, *eol; + size_t alloclen; + + pkt = git__malloc(sizeof(*pkt)); + GIT_ERROR_CHECK_ALLOC(pkt); + + pkt->ref = NULL; + pkt->type = GIT_PKT_NG; + + eol = line + len; + + if (git__prefixncmp(line, len, "ng ")) + goto out_err; + line += 3; + + if (!(ptr = memchr(line, ' ', eol - line))) + goto out_err; + len = ptr - line; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, len, 1); + pkt->ref = git__malloc(alloclen); + GIT_ERROR_CHECK_ALLOC(pkt->ref); + + memcpy(pkt->ref, line, len); + pkt->ref[len] = '\0'; + + line = ptr + 1; + if (line >= eol) + goto out_err; + + if (!(ptr = memchr(line, '\n', eol - line))) + goto out_err; + len = ptr - line; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, len, 1); + pkt->msg = git__malloc(alloclen); + GIT_ERROR_CHECK_ALLOC(pkt->msg); + + memcpy(pkt->msg, line, len); + pkt->msg[len] = '\0'; + + *out = (git_pkt *)pkt; + return 0; + +out_err: + git_error_set(GIT_ERROR_NET, "invalid packet line"); + git__free(pkt->ref); + git__free(pkt); + return -1; +} + +static int unpack_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_unpack *pkt; + + pkt = git__malloc(sizeof(*pkt)); + GIT_ERROR_CHECK_ALLOC(pkt); + pkt->type = GIT_PKT_UNPACK; + + if (!git__prefixncmp(line, len, "unpack ok")) + pkt->unpack_ok = 1; + else + pkt->unpack_ok = 0; + + *out = (git_pkt *)pkt; + return 0; +} + +static int shallow_pkt( + git_pkt **out, + const char *line, + size_t len, + git_pkt_parse_data *data) +{ + git_pkt_shallow *pkt; + size_t oid_hexsize = git_oid_hexsize(data->oid_type); + + GIT_ASSERT(data && data->oid_type); + + pkt = git__calloc(1, sizeof(git_pkt_shallow)); + GIT_ERROR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_SHALLOW; + + if (git__prefixncmp(line, len, "shallow ")) + goto out_err; + + line += 8; + len -= 8; + + if (len != oid_hexsize) + goto out_err; + + git_oid__fromstr(&pkt->oid, line, data->oid_type); + line += oid_hexsize + 1; + len -= oid_hexsize + 1; + + *out = (git_pkt *)pkt; + + return 0; + +out_err: + git_error_set(GIT_ERROR_NET, "invalid packet line"); + git__free(pkt); + return -1; +} + +static int unshallow_pkt( + git_pkt **out, + const char *line, + size_t len, + git_pkt_parse_data *data) +{ + git_pkt_shallow *pkt; + size_t oid_hexsize = git_oid_hexsize(data->oid_type); + + GIT_ASSERT(data && data->oid_type); + + pkt = git__calloc(1, sizeof(git_pkt_shallow)); + GIT_ERROR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_UNSHALLOW; + + if (git__prefixncmp(line, len, "unshallow ")) + goto out_err; + + line += 10; + len -= 10; + + if (len != oid_hexsize) + goto out_err; + + git_oid__fromstr(&pkt->oid, line, data->oid_type); + line += oid_hexsize + 1; + len -= oid_hexsize + 1; + + *out = (git_pkt *) pkt; + + return 0; + +out_err: + git_error_set(GIT_ERROR_NET, "invalid packet line"); + git__free(pkt); + return -1; +} + +static int parse_len(size_t *out, const char *line, size_t linelen) +{ + char num[PKT_LEN_SIZE + 1]; + int i, k, error; + int32_t len; + const char *num_end; + + /* Not even enough for the length */ + if (linelen < PKT_LEN_SIZE) + return GIT_EBUFS; + + memcpy(num, line, PKT_LEN_SIZE); + num[PKT_LEN_SIZE] = '\0'; + + for (i = 0; i < PKT_LEN_SIZE; ++i) { + if (!isxdigit(num[i])) { + /* Make sure there are no special characters before passing to error message */ + for (k = 0; k < PKT_LEN_SIZE; ++k) { + if(!isprint(num[k])) { + num[k] = '.'; + } + } + + git_error_set(GIT_ERROR_NET, "invalid hex digit in length: '%s'", num); + return -1; + } + } + + if ((error = git__strntol32(&len, num, PKT_LEN_SIZE, &num_end, 16)) < 0) + return error; + + if (len < 0) + return -1; + + *out = (size_t) len; + return 0; +} + +/* + * As per the documentation, the syntax is: + * + * pkt-line = data-pkt / flush-pkt + * data-pkt = pkt-len pkt-payload + * pkt-len = 4*(HEXDIG) + * pkt-payload = (pkt-len -4)*(OCTET) + * flush-pkt = "0000" + * + * Which means that the first four bytes are the length of the line, + * in ASCII hexadecimal (including itself) + */ + +int git_pkt_parse_line( + git_pkt **pkt, + const char **endptr, + const char *line, + size_t linelen, + git_pkt_parse_data *data) +{ + int error; + size_t len; + + if ((error = parse_len(&len, line, linelen)) < 0) { + /* + * If we fail to parse the length, it might be + * because the server is trying to send us the + * packfile already or because we do not yet have + * enough data. + */ + if (error == GIT_EBUFS) + ; + else if (!git__prefixncmp(line, linelen, "PACK")) + git_error_set(GIT_ERROR_NET, "unexpected pack file"); + else + git_error_set(GIT_ERROR_NET, "bad packet length"); + return error; + } + + /* + * Make sure there is enough in the buffer to satisfy + * this line. + */ + if (linelen < len) + return GIT_EBUFS; + + /* + * The length has to be exactly 0 in case of a flush + * packet or greater than PKT_LEN_SIZE, as the decoded + * length includes its own encoded length of four bytes. + */ + if (len != 0 && len < PKT_LEN_SIZE) + return GIT_ERROR; + + line += PKT_LEN_SIZE; + /* + * The Git protocol does not specify empty lines as part + * of the protocol. Not knowing what to do with an empty + * line, we should return an error upon hitting one. + */ + if (len == PKT_LEN_SIZE) { + git_error_set_str(GIT_ERROR_NET, "Invalid empty packet"); + return GIT_ERROR; + } + + if (len == 0) { /* Flush pkt */ + *endptr = line; + return flush_pkt(pkt); + } + + len -= PKT_LEN_SIZE; /* the encoded length includes its own size */ + + if (*line == GIT_SIDE_BAND_DATA) + error = data_pkt(pkt, line, len); + else if (*line == GIT_SIDE_BAND_PROGRESS) + error = sideband_progress_pkt(pkt, line, len); + else if (*line == GIT_SIDE_BAND_ERROR) + error = sideband_error_pkt(pkt, line, len); + else if (!git__prefixncmp(line, len, "ACK")) + error = ack_pkt(pkt, line, len, data); + else if (!git__prefixncmp(line, len, "NAK")) + error = nak_pkt(pkt); + else if (!git__prefixncmp(line, len, "ERR")) + error = err_pkt(pkt, line, len); + else if (*line == '#') + error = comment_pkt(pkt, line, len); + else if (!git__prefixncmp(line, len, "ok")) + error = ok_pkt(pkt, line, len); + else if (!git__prefixncmp(line, len, "ng")) + error = ng_pkt(pkt, line, len); + else if (!git__prefixncmp(line, len, "unpack")) + error = unpack_pkt(pkt, line, len); + else if (!git__prefixcmp(line, "shallow")) + error = shallow_pkt(pkt, line, len, data); + else if (!git__prefixcmp(line, "unshallow")) + error = unshallow_pkt(pkt, line, len, data); + else + error = ref_pkt(pkt, line, len, data); + + *endptr = line + len; + + return error; +} + +void git_pkt_free(git_pkt *pkt) +{ + if (pkt == NULL) { + return; + } + if (pkt->type == GIT_PKT_REF) { + git_pkt_ref *p = (git_pkt_ref *) pkt; + git__free(p->head.name); + git__free(p->head.symref_target); + } + + if (pkt->type == GIT_PKT_OK) { + git_pkt_ok *p = (git_pkt_ok *) pkt; + git__free(p->ref); + } + + if (pkt->type == GIT_PKT_NG) { + git_pkt_ng *p = (git_pkt_ng *) pkt; + git__free(p->ref); + git__free(p->msg); + } + + git__free(pkt); +} + +int git_pkt_buffer_flush(git_str *buf) +{ + return git_str_put(buf, PKT_FLUSH_STR, CONST_STRLEN(PKT_FLUSH_STR)); +} + +static int buffer_want_with_caps( + const git_remote_head *head, + transport_smart_caps *caps, + git_oid_t oid_type, + git_str *buf) +{ + git_str str = GIT_STR_INIT; + char oid[GIT_OID_MAX_HEXSIZE]; + size_t oid_hexsize, len; + + oid_hexsize = git_oid_hexsize(oid_type); + git_oid_fmt(oid, &head->oid); + + /* Prefer multi_ack_detailed */ + if (caps->multi_ack_detailed) + git_str_puts(&str, GIT_CAP_MULTI_ACK_DETAILED " "); + else if (caps->multi_ack) + git_str_puts(&str, GIT_CAP_MULTI_ACK " "); + + /* Prefer side-band-64k if the server supports both */ + if (caps->side_band_64k) + git_str_printf(&str, "%s ", GIT_CAP_SIDE_BAND_64K); + else if (caps->side_band) + git_str_printf(&str, "%s ", GIT_CAP_SIDE_BAND); + + if (caps->include_tag) + git_str_puts(&str, GIT_CAP_INCLUDE_TAG " "); + + if (caps->thin_pack) + git_str_puts(&str, GIT_CAP_THIN_PACK " "); + + if (caps->ofs_delta) + git_str_puts(&str, GIT_CAP_OFS_DELTA " "); + + if (caps->shallow) + git_str_puts(&str, GIT_CAP_SHALLOW " "); + + if (git_str_oom(&str)) + return -1; + + if (str.size > (PKT_MAX_SIZE - (PKT_MAX_WANTLEN + 1))) { + git_error_set(GIT_ERROR_NET, + "tried to produce packet with invalid caps length %" PRIuZ, str.size); + return -1; + } + + len = PKT_LEN_SIZE + CONST_STRLEN(PKT_WANT_PREFIX) + + oid_hexsize + 1 /* NUL */ + + git_str_len(&str) + 1 /* LF */; + + git_str_grow_by(buf, len); + git_str_printf(buf, + "%04x%s%.*s %s\n", (unsigned int)len, PKT_WANT_PREFIX, + (int)oid_hexsize, oid, git_str_cstr(&str)); + git_str_dispose(&str); + + GIT_ERROR_CHECK_ALLOC_STR(buf); + + return 0; +} + +/* + * All "want" packets have the same length and format, so what we do + * is overwrite the OID each time. + */ + +int git_pkt_buffer_wants( + const git_fetch_negotiation *wants, + transport_smart_caps *caps, + git_str *buf) +{ + const git_remote_head *head; + char oid[GIT_OID_MAX_HEXSIZE]; + git_oid_t oid_type; + size_t oid_hexsize, want_len, i = 0; + +#ifdef GIT_EXPERIMENTAL_SHA256 + oid_type = wants->refs_len > 0 ? wants->refs[0]->oid.type : GIT_OID_SHA1; +#else + oid_type = GIT_OID_SHA1; +#endif + + oid_hexsize = git_oid_hexsize(oid_type); + + want_len = PKT_LEN_SIZE + CONST_STRLEN(PKT_WANT_PREFIX) + + oid_hexsize + 1 /* LF */; + + if (caps->common) { + for (; i < wants->refs_len; ++i) { + head = wants->refs[i]; + if (!head->local) + break; + } + + if (buffer_want_with_caps(wants->refs[i], caps, oid_type, buf) < 0) + return -1; + + i++; + } + + for (; i < wants->refs_len; ++i) { + head = wants->refs[i]; + + if (head->local) + continue; + + git_oid_fmt(oid, &head->oid); + + git_str_printf(buf, "%04x%s%.*s\n", + (unsigned int)want_len, PKT_WANT_PREFIX, + (int)oid_hexsize, oid); + + if (git_str_oom(buf)) + return -1; + } + + /* Tell the server about our shallow objects */ + for (i = 0; i < wants->shallow_roots_len; i++) { + char oid[GIT_OID_MAX_HEXSIZE + 1]; + git_str shallow_buf = GIT_STR_INIT; + + git_oid_tostr(oid, GIT_OID_MAX_HEXSIZE + 1, &wants->shallow_roots[i]); + git_str_puts(&shallow_buf, "shallow "); + git_str_puts(&shallow_buf, oid); + git_str_putc(&shallow_buf, '\n'); + + git_str_printf(buf, "%04x%s", (unsigned int)git_str_len(&shallow_buf) + 4, git_str_cstr(&shallow_buf)); + + git_str_dispose(&shallow_buf); + + if (git_str_oom(buf)) + return -1; + } + + if (wants->depth > 0) { + git_str deepen_buf = GIT_STR_INIT; + + git_str_printf(&deepen_buf, "deepen %d\n", wants->depth); + git_str_printf(buf,"%04x%s", (unsigned int)git_str_len(&deepen_buf) + 4, git_str_cstr(&deepen_buf)); + + git_str_dispose(&deepen_buf); + + if (git_str_oom(buf)) + return -1; + } + + return git_pkt_buffer_flush(buf); +} + +int git_pkt_buffer_have(git_oid *oid, git_str *buf) +{ + char oid_str[GIT_OID_MAX_HEXSIZE]; + git_oid_t oid_type; + size_t oid_hexsize, have_len; + +#ifdef GIT_EXPERIMENTAL_SHA256 + oid_type = oid->type; +#else + oid_type = GIT_OID_SHA1; +#endif + + oid_hexsize = git_oid_hexsize(oid_type); + have_len = PKT_LEN_SIZE + CONST_STRLEN(PKT_HAVE_PREFIX) + + oid_hexsize + 1 /* LF */; + + git_oid_fmt(oid_str, oid); + return git_str_printf(buf, "%04x%s%.*s\n", + (unsigned int)have_len, PKT_HAVE_PREFIX, + (int)oid_hexsize, oid_str); +} + +int git_pkt_buffer_done(git_str *buf) +{ + return git_str_put(buf, PKT_DONE_STR, CONST_STRLEN(PKT_DONE_STR)); +} diff --git a/src/libgit2/transports/smart_protocol.c b/src/libgit2/transports/smart_protocol.c new file mode 100644 index 0000000..c9c422d --- /dev/null +++ b/src/libgit2/transports/smart_protocol.c @@ -0,0 +1,1231 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#include "git2.h" +#include "git2/odb_backend.h" + +#include "smart.h" +#include "refs.h" +#include "repository.h" +#include "push.h" +#include "pack-objects.h" +#include "remote.h" +#include "util.h" +#include "revwalk.h" + +#define NETWORK_XFER_THRESHOLD (100*1024) +/* The minimal interval between progress updates (in seconds). */ +#define MIN_PROGRESS_UPDATE_INTERVAL 0.5 + +bool git_smart__ofs_delta_enabled = true; + +int git_smart__store_refs(transport_smart *t, int flushes) +{ + git_vector *refs = &t->refs; + int error, flush = 0, recvd; + const char *line_end = NULL; + git_pkt *pkt = NULL; + git_pkt_parse_data pkt_parse_data = { 0 }; + size_t i; + + /* Clear existing refs in case git_remote_connect() is called again + * after git_remote_disconnect(). + */ + git_vector_foreach(refs, i, pkt) { + git_pkt_free(pkt); + } + git_vector_clear(refs); + pkt = NULL; + + do { + if (t->buffer.len > 0) + error = git_pkt_parse_line(&pkt, &line_end, + t->buffer.data, t->buffer.len, + &pkt_parse_data); + else + error = GIT_EBUFS; + + if (error < 0 && error != GIT_EBUFS) + return error; + + if (error == GIT_EBUFS) { + if ((recvd = git_smart__recv(t)) < 0) + return recvd; + + if (recvd == 0) { + git_error_set(GIT_ERROR_NET, "early EOF"); + return GIT_EEOF; + } + + continue; + } + + git_staticstr_consume(&t->buffer, line_end); + + if (pkt->type == GIT_PKT_ERR) { + git_error_set(GIT_ERROR_NET, "remote error: %s", ((git_pkt_err *)pkt)->error); + git__free(pkt); + return -1; + } + + if (pkt->type != GIT_PKT_FLUSH && git_vector_insert(refs, pkt) < 0) + return -1; + + if (pkt->type == GIT_PKT_FLUSH) { + flush++; + git_pkt_free(pkt); + } + } while (flush < flushes); + + return flush; +} + +static int append_symref(const char **out, git_vector *symrefs, const char *ptr) +{ + int error; + const char *end; + git_str buf = GIT_STR_INIT; + git_refspec *mapping = NULL; + + ptr += strlen(GIT_CAP_SYMREF); + if (*ptr != '=') + goto on_invalid; + + ptr++; + if (!(end = strchr(ptr, ' ')) && + !(end = strchr(ptr, '\0'))) + goto on_invalid; + + if ((error = git_str_put(&buf, ptr, end - ptr)) < 0) + return error; + + /* symref mapping has refspec format */ + mapping = git__calloc(1, sizeof(git_refspec)); + GIT_ERROR_CHECK_ALLOC(mapping); + + error = git_refspec__parse(mapping, git_str_cstr(&buf), true); + git_str_dispose(&buf); + + /* if the error isn't OOM, then it's a parse error; let's use a nicer message */ + if (error < 0) { + if (git_error_last()->klass != GIT_ERROR_NOMEMORY) + goto on_invalid; + + git__free(mapping); + return error; + } + + if ((error = git_vector_insert(symrefs, mapping)) < 0) + return error; + + *out = end; + return 0; + +on_invalid: + git_error_set(GIT_ERROR_NET, "remote sent invalid symref"); + git_refspec__dispose(mapping); + git__free(mapping); + return -1; +} + +int git_smart__detect_caps( + git_pkt_ref *pkt, + transport_smart_caps *caps, + git_vector *symrefs) +{ + const char *ptr, *start; + + /* No refs or capabilities, odd but not a problem */ + if (pkt == NULL || pkt->capabilities == NULL) + return GIT_ENOTFOUND; + + ptr = pkt->capabilities; + while (ptr != NULL && *ptr != '\0') { + if (*ptr == ' ') + ptr++; + + if (git_smart__ofs_delta_enabled && !git__prefixcmp(ptr, GIT_CAP_OFS_DELTA)) { + caps->common = caps->ofs_delta = 1; + ptr += strlen(GIT_CAP_OFS_DELTA); + continue; + } + + /* Keep multi_ack_detailed before multi_ack */ + if (!git__prefixcmp(ptr, GIT_CAP_MULTI_ACK_DETAILED)) { + caps->common = caps->multi_ack_detailed = 1; + ptr += strlen(GIT_CAP_MULTI_ACK_DETAILED); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_MULTI_ACK)) { + caps->common = caps->multi_ack = 1; + ptr += strlen(GIT_CAP_MULTI_ACK); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_INCLUDE_TAG)) { + caps->common = caps->include_tag = 1; + ptr += strlen(GIT_CAP_INCLUDE_TAG); + continue; + } + + /* Keep side-band check after side-band-64k */ + if (!git__prefixcmp(ptr, GIT_CAP_SIDE_BAND_64K)) { + caps->common = caps->side_band_64k = 1; + ptr += strlen(GIT_CAP_SIDE_BAND_64K); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_SIDE_BAND)) { + caps->common = caps->side_band = 1; + ptr += strlen(GIT_CAP_SIDE_BAND); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_DELETE_REFS)) { + caps->common = caps->delete_refs = 1; + ptr += strlen(GIT_CAP_DELETE_REFS); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_THIN_PACK)) { + caps->common = caps->thin_pack = 1; + ptr += strlen(GIT_CAP_THIN_PACK); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_SYMREF)) { + int error; + + if ((error = append_symref(&ptr, symrefs, ptr)) < 0) + return error; + + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_WANT_TIP_SHA1)) { + caps->common = caps->want_tip_sha1 = 1; + ptr += strlen(GIT_CAP_WANT_TIP_SHA1); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_WANT_REACHABLE_SHA1)) { + caps->common = caps->want_reachable_sha1 = 1; + ptr += strlen(GIT_CAP_WANT_REACHABLE_SHA1); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_OBJECT_FORMAT)) { + ptr += strlen(GIT_CAP_OBJECT_FORMAT); + + start = ptr; + ptr = strchr(ptr, ' '); + + if ((caps->object_format = git__strndup(start, (ptr - start))) == NULL) + return -1; + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_AGENT)) { + ptr += strlen(GIT_CAP_AGENT); + + start = ptr; + ptr = strchr(ptr, ' '); + + if ((caps->agent = git__strndup(start, (ptr - start))) == NULL) + return -1; + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_SHALLOW)) { + caps->common = caps->shallow = 1; + ptr += strlen(GIT_CAP_SHALLOW); + continue; + } + + /* We don't know this capability, so skip it */ + ptr = strchr(ptr, ' '); + } + + return 0; +} + +static int recv_pkt( + git_pkt **out_pkt, + git_pkt_type *out_type, + transport_smart *t) +{ + const char *ptr = t->buffer.data, *line_end = ptr; + git_pkt *pkt = NULL; + git_pkt_parse_data pkt_parse_data = { 0 }; + int error = 0, ret; + + pkt_parse_data.oid_type = t->owner->repo->oid_type; + pkt_parse_data.seen_capabilities = 1; + + do { + if (t->buffer.len > 0) + error = git_pkt_parse_line(&pkt, &line_end, ptr, + t->buffer.len, &pkt_parse_data); + else + error = GIT_EBUFS; + + if (error == 0) + break; /* return the pkt */ + + if (error < 0 && error != GIT_EBUFS) + return error; + + if ((ret = git_smart__recv(t)) < 0) { + return ret; + } else if (ret == 0) { + git_error_set(GIT_ERROR_NET, "early EOF"); + return GIT_EEOF; + } + } while (error); + + git_staticstr_consume(&t->buffer, line_end); + + if (out_type != NULL) + *out_type = pkt->type; + if (out_pkt != NULL) + *out_pkt = pkt; + else + git__free(pkt); + + return error; +} + +static int store_common(transport_smart *t) +{ + git_pkt *pkt = NULL; + int error; + + do { + if ((error = recv_pkt(&pkt, NULL, t)) < 0) + return error; + + if (pkt->type != GIT_PKT_ACK) { + git__free(pkt); + return 0; + } + + if (git_vector_insert(&t->common, pkt) < 0) { + git__free(pkt); + return -1; + } + } while (1); + + return 0; +} + +static int wait_while_ack(transport_smart *t) +{ + int error; + git_pkt *pkt = NULL; + git_pkt_ack *ack = NULL; + + while (1) { + git_pkt_free(pkt); + + if ((error = recv_pkt(&pkt, NULL, t)) < 0) + return error; + + if (pkt->type == GIT_PKT_NAK) + break; + if (pkt->type != GIT_PKT_ACK) + continue; + + ack = (git_pkt_ack*)pkt; + + if (ack->status != GIT_ACK_CONTINUE && + ack->status != GIT_ACK_COMMON && + ack->status != GIT_ACK_READY) { + break; + } + } + + git_pkt_free(pkt); + return 0; +} + +static int cap_not_sup_err(const char *cap_name) +{ + git_error_set(GIT_ERROR_NET, "server doesn't support %s", cap_name); + return GIT_EINVALID; +} + +/* Disables server capabilities we're not interested in */ +static int setup_caps( + transport_smart_caps *caps, + const git_fetch_negotiation *wants) +{ + if (wants->depth > 0) { + if (!caps->shallow) + return cap_not_sup_err(GIT_CAP_SHALLOW); + } else { + caps->shallow = 0; + } + + return 0; +} + +static int setup_shallow_roots( + git_array_oid_t *out, + const git_fetch_negotiation *wants) +{ + git_array_clear(*out); + + if (wants->shallow_roots_len > 0) { + git_array_init_to_size(*out, wants->shallow_roots_len); + GIT_ERROR_CHECK_ALLOC(out->ptr); + + memcpy(out->ptr, wants->shallow_roots, + sizeof(git_oid) * wants->shallow_roots_len); + } + + return 0; +} + +int git_smart__negotiate_fetch( + git_transport *transport, + git_repository *repo, + const git_fetch_negotiation *wants) +{ + transport_smart *t = (transport_smart *)transport; + git_revwalk__push_options opts = GIT_REVWALK__PUSH_OPTIONS_INIT; + git_str data = GIT_STR_INIT; + git_revwalk *walk = NULL; + int error = -1; + git_pkt_type pkt_type; + unsigned int i; + git_oid oid; + + if ((error = setup_caps(&t->caps, wants)) < 0 || + (error = setup_shallow_roots(&t->shallow_roots, wants)) < 0) + return error; + + if ((error = git_pkt_buffer_wants(wants, &t->caps, &data)) < 0) + return error; + + if ((error = git_revwalk_new(&walk, repo)) < 0) + goto on_error; + + opts.insert_by_date = 1; + if ((error = git_revwalk__push_glob(walk, "refs/*", &opts)) < 0) + goto on_error; + + if (wants->depth > 0) { + git_pkt_shallow *pkt; + + if ((error = git_smart__negotiation_step(&t->parent, data.ptr, data.size)) < 0) + goto on_error; + + while ((error = recv_pkt((git_pkt **)&pkt, NULL, t)) == 0) { + bool complete = false; + + if (pkt->type == GIT_PKT_SHALLOW) { + error = git_oidarray__add(&t->shallow_roots, &pkt->oid); + } else if (pkt->type == GIT_PKT_UNSHALLOW) { + git_oidarray__remove(&t->shallow_roots, &pkt->oid); + } else if (pkt->type == GIT_PKT_FLUSH) { + /* Server is done, stop processing shallow oids */ + complete = true; + } else { + git_error_set(GIT_ERROR_NET, "unexpected packet type"); + error = -1; + } + + git_pkt_free((git_pkt *) pkt); + + if (complete || error < 0) + break; + } + + if (error < 0) + goto on_error; + } + + /* + * Our support for ACK extensions is simply to parse them. On + * the first ACK we will accept that as enough common + * objects. We give up if we haven't found an answer in the + * first 256 we send. + */ + i = 0; + while (i < 256) { + error = git_revwalk_next(&oid, walk); + + if (error < 0) { + if (GIT_ITEROVER == error) + break; + + goto on_error; + } + + git_pkt_buffer_have(&oid, &data); + i++; + if (i % 20 == 0) { + if (t->cancelled.val) { + git_error_set(GIT_ERROR_NET, "The fetch was cancelled by the user"); + error = GIT_EUSER; + goto on_error; + } + + git_pkt_buffer_flush(&data); + if (git_str_oom(&data)) { + error = -1; + goto on_error; + } + + if ((error = git_smart__negotiation_step(&t->parent, data.ptr, data.size)) < 0) + goto on_error; + + git_str_clear(&data); + if (t->caps.multi_ack || t->caps.multi_ack_detailed) { + if ((error = store_common(t)) < 0) + goto on_error; + } else { + if ((error = recv_pkt(NULL, &pkt_type, t)) < 0) + goto on_error; + + if (pkt_type == GIT_PKT_ACK) { + break; + } else if (pkt_type == GIT_PKT_NAK) { + continue; + } else { + git_error_set(GIT_ERROR_NET, "unexpected pkt type"); + error = -1; + goto on_error; + } + } + } + + if (t->common.length > 0) + break; + + if (i % 20 == 0 && t->rpc) { + git_pkt_ack *pkt; + unsigned int j; + + if ((error = git_pkt_buffer_wants(wants, &t->caps, &data)) < 0) + goto on_error; + + git_vector_foreach(&t->common, j, pkt) { + if ((error = git_pkt_buffer_have(&pkt->oid, &data)) < 0) + goto on_error; + } + + if (git_str_oom(&data)) { + error = -1; + goto on_error; + } + } + } + + /* Tell the other end that we're done negotiating */ + if (t->rpc && t->common.length > 0) { + git_pkt_ack *pkt; + unsigned int j; + + if ((error = git_pkt_buffer_wants(wants, &t->caps, &data)) < 0) + goto on_error; + + git_vector_foreach(&t->common, j, pkt) { + if ((error = git_pkt_buffer_have(&pkt->oid, &data)) < 0) + goto on_error; + } + + if (git_str_oom(&data)) { + error = -1; + goto on_error; + } + } + + if ((error = git_pkt_buffer_done(&data)) < 0) + goto on_error; + + if (t->cancelled.val) { + git_error_set(GIT_ERROR_NET, "the fetch was cancelled"); + error = GIT_EUSER; + goto on_error; + } + + if ((error = git_smart__negotiation_step(&t->parent, data.ptr, data.size)) < 0) + goto on_error; + + git_str_dispose(&data); + git_revwalk_free(walk); + + /* Now let's eat up whatever the server gives us */ + if (!t->caps.multi_ack && !t->caps.multi_ack_detailed) { + if ((error = recv_pkt(NULL, &pkt_type, t)) < 0) + return error; + + if (pkt_type != GIT_PKT_ACK && pkt_type != GIT_PKT_NAK) { + git_error_set(GIT_ERROR_NET, "unexpected pkt type"); + return -1; + } + } else { + error = wait_while_ack(t); + } + + return error; + +on_error: + git_revwalk_free(walk); + git_str_dispose(&data); + return error; +} + +int git_smart__shallow_roots(git_oidarray *out, git_transport *transport) +{ + transport_smart *t = (transport_smart *)transport; + size_t len; + + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&len, t->shallow_roots.size, sizeof(git_oid)); + + out->count = t->shallow_roots.size; + + if (len) { + out->ids = git__malloc(len); + memcpy(out->ids, t->shallow_roots.ptr, len); + } else { + out->ids = NULL; + } + + return 0; +} + +static int no_sideband( + transport_smart *t, + struct git_odb_writepack *writepack, + git_indexer_progress *stats) +{ + int recvd; + + do { + if (t->cancelled.val) { + git_error_set(GIT_ERROR_NET, "the fetch was cancelled by the user"); + return GIT_EUSER; + } + + if (writepack->append(writepack, t->buffer.data, t->buffer.len, stats) < 0) + return -1; + + git_staticstr_clear(&t->buffer); + + if ((recvd = git_smart__recv(t)) < 0) + return recvd; + } while(recvd > 0); + + if (writepack->commit(writepack, stats) < 0) + return -1; + + return 0; +} + +struct network_packetsize_payload +{ + git_indexer_progress_cb callback; + void *payload; + git_indexer_progress *stats; + size_t last_fired_bytes; +}; + +static int network_packetsize(size_t received, void *payload) +{ + struct network_packetsize_payload *npp = (struct network_packetsize_payload*)payload; + + /* Accumulate bytes */ + npp->stats->received_bytes += received; + + /* Fire notification if the threshold is reached */ + if ((npp->stats->received_bytes - npp->last_fired_bytes) > NETWORK_XFER_THRESHOLD) { + npp->last_fired_bytes = npp->stats->received_bytes; + + if (npp->callback(npp->stats, npp->payload)) + return GIT_EUSER; + } + + return 0; +} + +int git_smart__download_pack( + git_transport *transport, + git_repository *repo, + git_indexer_progress *stats) +{ + transport_smart *t = (transport_smart *)transport; + git_odb *odb; + struct git_odb_writepack *writepack = NULL; + int error = 0; + struct network_packetsize_payload npp = {0}; + + git_indexer_progress_cb progress_cb = t->connect_opts.callbacks.transfer_progress; + void *progress_payload = t->connect_opts.callbacks.payload; + + memset(stats, 0, sizeof(git_indexer_progress)); + + if (progress_cb) { + npp.callback = progress_cb; + npp.payload = progress_payload; + npp.stats = stats; + t->packetsize_cb = &network_packetsize; + t->packetsize_payload = &npp; + + /* We might have something in the buffer already from negotiate_fetch */ + if (t->buffer.len > 0 && !t->cancelled.val) { + if (t->packetsize_cb(t->buffer.len, t->packetsize_payload)) + git_atomic32_set(&t->cancelled, 1); + } + } + + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0 || + ((error = git_odb_write_pack(&writepack, odb, progress_cb, progress_payload)) != 0)) + goto done; + + /* + * If the remote doesn't support the side-band, we can feed + * the data directly to the pack writer. Otherwise, we need to + * check which one belongs there. + */ + if (!t->caps.side_band && !t->caps.side_band_64k) { + error = no_sideband(t, writepack, stats); + goto done; + } + + do { + git_pkt *pkt = NULL; + + /* Check cancellation before network call */ + if (t->cancelled.val) { + git_error_clear(); + error = GIT_EUSER; + goto done; + } + + if ((error = recv_pkt(&pkt, NULL, t)) >= 0) { + /* Check cancellation after network call */ + if (t->cancelled.val) { + git_error_clear(); + error = GIT_EUSER; + } else if (pkt->type == GIT_PKT_PROGRESS) { + if (t->connect_opts.callbacks.sideband_progress) { + git_pkt_progress *p = (git_pkt_progress *) pkt; + + if (p->len > INT_MAX) { + git_error_set(GIT_ERROR_NET, "oversized progress message"); + error = GIT_ERROR; + goto done; + } + + error = t->connect_opts.callbacks.sideband_progress(p->data, (int)p->len, t->connect_opts.callbacks.payload); + } + } else if (pkt->type == GIT_PKT_DATA) { + git_pkt_data *p = (git_pkt_data *) pkt; + + if (p->len) + error = writepack->append(writepack, p->data, p->len, stats); + } else if (pkt->type == GIT_PKT_FLUSH) { + /* A flush indicates the end of the packfile */ + git__free(pkt); + break; + } + } + + git_pkt_free(pkt); + + if (error < 0) + goto done; + + } while (1); + + /* + * Trailing execution of progress_cb, if necessary... + * Only the callback through the npp datastructure currently + * updates the last_fired_bytes value. It is possible that + * progress has already been reported with the correct + * "received_bytes" value, but until (if?) this is unified + * then we will report progress again to be sure that the + * correct last received_bytes value is reported. + */ + if (npp.callback && npp.stats->received_bytes > npp.last_fired_bytes) { + error = npp.callback(npp.stats, npp.payload); + if (error != 0) + goto done; + } + + error = writepack->commit(writepack, stats); + +done: + if (writepack) + writepack->free(writepack); + if (progress_cb) { + t->packetsize_cb = NULL; + t->packetsize_payload = NULL; + } + + return error; +} + +static int gen_pktline(git_str *buf, git_push *push) +{ + push_spec *spec; + size_t i, len; + char old_id[GIT_OID_SHA1_HEXSIZE+1], new_id[GIT_OID_SHA1_HEXSIZE+1]; + + old_id[GIT_OID_SHA1_HEXSIZE] = '\0'; new_id[GIT_OID_SHA1_HEXSIZE] = '\0'; + + git_vector_foreach(&push->specs, i, spec) { + len = 2*GIT_OID_SHA1_HEXSIZE + 7 + strlen(spec->refspec.dst); + + if (i == 0) { + ++len; /* '\0' */ + if (push->report_status) + len += strlen(GIT_CAP_REPORT_STATUS) + 1; + len += strlen(GIT_CAP_SIDE_BAND_64K) + 1; + } + + git_oid_fmt(old_id, &spec->roid); + git_oid_fmt(new_id, &spec->loid); + + git_str_printf(buf, "%04"PRIxZ"%s %s %s", len, old_id, new_id, spec->refspec.dst); + + if (i == 0) { + git_str_putc(buf, '\0'); + /* Core git always starts their capabilities string with a space */ + if (push->report_status) { + git_str_putc(buf, ' '); + git_str_printf(buf, GIT_CAP_REPORT_STATUS); + } + git_str_putc(buf, ' '); + git_str_printf(buf, GIT_CAP_SIDE_BAND_64K); + } + + git_str_putc(buf, '\n'); + } + + git_str_puts(buf, "0000"); + return git_str_oom(buf) ? -1 : 0; +} + +static int add_push_report_pkt(git_push *push, git_pkt *pkt) +{ + push_status *status; + + switch (pkt->type) { + case GIT_PKT_OK: + status = git__calloc(1, sizeof(push_status)); + GIT_ERROR_CHECK_ALLOC(status); + status->msg = NULL; + status->ref = git__strdup(((git_pkt_ok *)pkt)->ref); + if (!status->ref || + git_vector_insert(&push->status, status) < 0) { + git_push_status_free(status); + return -1; + } + break; + case GIT_PKT_NG: + status = git__calloc(1, sizeof(push_status)); + GIT_ERROR_CHECK_ALLOC(status); + status->ref = git__strdup(((git_pkt_ng *)pkt)->ref); + status->msg = git__strdup(((git_pkt_ng *)pkt)->msg); + if (!status->ref || !status->msg || + git_vector_insert(&push->status, status) < 0) { + git_push_status_free(status); + return -1; + } + break; + case GIT_PKT_UNPACK: + push->unpack_ok = ((git_pkt_unpack *)pkt)->unpack_ok; + break; + case GIT_PKT_FLUSH: + return GIT_ITEROVER; + default: + git_error_set(GIT_ERROR_NET, "report-status: protocol error"); + return -1; + } + + return 0; +} + +static int add_push_report_sideband_pkt(git_push *push, git_pkt_data *data_pkt, git_str *data_pkt_buf) +{ + git_pkt *pkt; + git_pkt_parse_data pkt_parse_data = { 0 }; + const char *line, *line_end = NULL; + size_t line_len; + int error; + int reading_from_buf = data_pkt_buf->size > 0; + + if (reading_from_buf) { + /* We had an existing partial packet, so add the new + * packet to the buffer and parse the whole thing */ + git_str_put(data_pkt_buf, data_pkt->data, data_pkt->len); + line = data_pkt_buf->ptr; + line_len = data_pkt_buf->size; + } + else { + line = data_pkt->data; + line_len = data_pkt->len; + } + + while (line_len > 0) { + error = git_pkt_parse_line(&pkt, &line_end, line, line_len, &pkt_parse_data); + + if (error == GIT_EBUFS) { + /* Buffer the data when the inner packet is split + * across multiple sideband packets */ + if (!reading_from_buf) + git_str_put(data_pkt_buf, line, line_len); + error = 0; + goto done; + } + else if (error < 0) + goto done; + + /* Advance in the buffer */ + line_len -= (line_end - line); + line = line_end; + + error = add_push_report_pkt(push, pkt); + + git_pkt_free(pkt); + + if (error < 0 && error != GIT_ITEROVER) + goto done; + } + + error = 0; + +done: + if (reading_from_buf) + git_str_consume(data_pkt_buf, line_end); + return error; +} + +static int parse_report(transport_smart *transport, git_push *push) +{ + git_pkt *pkt = NULL; + git_pkt_parse_data pkt_parse_data = { 0 }; + const char *line_end = NULL; + int error, recvd; + git_str data_pkt_buf = GIT_STR_INIT; + + for (;;) { + if (transport->buffer.len > 0) + error = git_pkt_parse_line(&pkt, &line_end, + transport->buffer.data, + transport->buffer.len, + &pkt_parse_data); + else + error = GIT_EBUFS; + + if (error < 0 && error != GIT_EBUFS) { + error = -1; + goto done; + } + + if (error == GIT_EBUFS) { + if ((recvd = git_smart__recv(transport)) < 0) { + error = recvd; + goto done; + } + + if (recvd == 0) { + git_error_set(GIT_ERROR_NET, "early EOF"); + error = GIT_EEOF; + goto done; + } + continue; + } + + git_staticstr_consume(&transport->buffer, line_end); + error = 0; + + switch (pkt->type) { + case GIT_PKT_DATA: + /* This is a sideband packet which contains other packets */ + error = add_push_report_sideband_pkt(push, (git_pkt_data *)pkt, &data_pkt_buf); + break; + case GIT_PKT_ERR: + git_error_set(GIT_ERROR_NET, "report-status: Error reported: %s", + ((git_pkt_err *)pkt)->error); + error = -1; + break; + case GIT_PKT_PROGRESS: + if (transport->connect_opts.callbacks.sideband_progress) { + git_pkt_progress *p = (git_pkt_progress *) pkt; + + if (p->len > INT_MAX) { + git_error_set(GIT_ERROR_NET, "oversized progress message"); + error = GIT_ERROR; + goto done; + } + + error = transport->connect_opts.callbacks.sideband_progress(p->data, (int)p->len, transport->connect_opts.callbacks.payload); + } + break; + default: + error = add_push_report_pkt(push, pkt); + break; + } + + git_pkt_free(pkt); + + /* add_push_report_pkt returns GIT_ITEROVER when it receives a flush */ + if (error == GIT_ITEROVER) { + error = 0; + if (data_pkt_buf.size > 0) { + /* If there was data remaining in the pack data buffer, + * then the server sent a partial pkt-line */ + git_error_set(GIT_ERROR_NET, "incomplete pack data pkt-line"); + error = GIT_ERROR; + } + goto done; + } + + if (error < 0) { + goto done; + } + } +done: + git_str_dispose(&data_pkt_buf); + return error; +} + +static int add_ref_from_push_spec(git_vector *refs, push_spec *push_spec) +{ + git_pkt_ref *added = git__calloc(1, sizeof(git_pkt_ref)); + GIT_ERROR_CHECK_ALLOC(added); + + added->type = GIT_PKT_REF; + git_oid_cpy(&added->head.oid, &push_spec->loid); + added->head.name = git__strdup(push_spec->refspec.dst); + + if (!added->head.name || + git_vector_insert(refs, added) < 0) { + git_pkt_free((git_pkt *)added); + return -1; + } + + return 0; +} + +static int update_refs_from_report( + git_vector *refs, + git_vector *push_specs, + git_vector *push_report) +{ + git_pkt_ref *ref; + push_spec *push_spec; + push_status *push_status; + size_t i, j, refs_len; + int cmp; + + /* For each push spec we sent to the server, we should have + * gotten back a status packet in the push report */ + if (push_specs->length != push_report->length) { + git_error_set(GIT_ERROR_NET, "report-status: protocol error"); + return -1; + } + + /* We require that push_specs be sorted with push_spec_rref_cmp, + * and that push_report be sorted with push_status_ref_cmp */ + git_vector_sort(push_specs); + git_vector_sort(push_report); + + git_vector_foreach(push_specs, i, push_spec) { + push_status = git_vector_get(push_report, i); + + /* For each push spec we sent to the server, we should have + * gotten back a status packet in the push report which matches */ + if (strcmp(push_spec->refspec.dst, push_status->ref)) { + git_error_set(GIT_ERROR_NET, "report-status: protocol error"); + return -1; + } + } + + /* We require that refs be sorted with ref_name_cmp */ + git_vector_sort(refs); + i = j = 0; + refs_len = refs->length; + + /* Merge join push_specs with refs */ + while (i < push_specs->length && j < refs_len) { + push_spec = git_vector_get(push_specs, i); + push_status = git_vector_get(push_report, i); + ref = git_vector_get(refs, j); + + cmp = strcmp(push_spec->refspec.dst, ref->head.name); + + /* Iterate appropriately */ + if (cmp <= 0) i++; + if (cmp >= 0) j++; + + /* Add case */ + if (cmp < 0 && + !push_status->msg && + add_ref_from_push_spec(refs, push_spec) < 0) + return -1; + + /* Update case, delete case */ + if (cmp == 0 && + !push_status->msg) + git_oid_cpy(&ref->head.oid, &push_spec->loid); + } + + for (; i < push_specs->length; i++) { + push_spec = git_vector_get(push_specs, i); + push_status = git_vector_get(push_report, i); + + /* Add case */ + if (!push_status->msg && + add_ref_from_push_spec(refs, push_spec) < 0) + return -1; + } + + /* Remove any refs which we updated to have a zero OID. */ + git_vector_rforeach(refs, i, ref) { + if (git_oid_is_zero(&ref->head.oid)) { + git_vector_remove(refs, i); + git_pkt_free((git_pkt *)ref); + } + } + + git_vector_sort(refs); + + return 0; +} + +struct push_packbuilder_payload +{ + git_smart_subtransport_stream *stream; + git_packbuilder *pb; + git_push_transfer_progress_cb cb; + void *cb_payload; + size_t last_bytes; + uint64_t last_progress_report_time; +}; + +static int stream_thunk(void *buf, size_t size, void *data) +{ + int error = 0; + struct push_packbuilder_payload *payload = data; + + if ((error = payload->stream->write(payload->stream, (const char *)buf, size)) < 0) + return error; + + if (payload->cb) { + uint64_t current_time = git_time_monotonic(); + uint64_t elapsed = current_time - payload->last_progress_report_time; + payload->last_bytes += size; + + if (elapsed >= MIN_PROGRESS_UPDATE_INTERVAL) { + payload->last_progress_report_time = current_time; + error = payload->cb(payload->pb->nr_written, payload->pb->nr_objects, payload->last_bytes, payload->cb_payload); + } + } + + return error; +} + +int git_smart__push(git_transport *transport, git_push *push) +{ + transport_smart *t = (transport_smart *)transport; + git_remote_callbacks *cbs = &t->connect_opts.callbacks; + struct push_packbuilder_payload packbuilder_payload = {0}; + git_str pktline = GIT_STR_INIT; + int error = 0, need_pack = 0; + push_spec *spec; + unsigned int i; + + packbuilder_payload.pb = push->pb; + + if (cbs && cbs->push_transfer_progress) { + packbuilder_payload.cb = cbs->push_transfer_progress; + packbuilder_payload.cb_payload = cbs->payload; + } + +#ifdef PUSH_DEBUG +{ + git_remote_head *head; + char hex[GIT_OID_SHA1_HEXSIZE+1]; hex[GIT_OID_SHA1_HEXSIZE] = '\0'; + + git_vector_foreach(&push->remote->refs, i, head) { + git_oid_fmt(hex, &head->oid); + fprintf(stderr, "%s (%s)\n", hex, head->name); + } + + git_vector_foreach(&push->specs, i, spec) { + git_oid_fmt(hex, &spec->roid); + fprintf(stderr, "%s (%s) -> ", hex, spec->lref); + git_oid_fmt(hex, &spec->loid); + fprintf(stderr, "%s (%s)\n", hex, spec->rref ? + spec->rref : spec->lref); + } +} +#endif + + /* + * Figure out if we need to send a packfile; which is in all + * cases except when we only send delete commands + */ + git_vector_foreach(&push->specs, i, spec) { + if (spec->refspec.src && spec->refspec.src[0] != '\0') { + need_pack = 1; + break; + } + } + + /* prepare pack before sending pack header to avoid timeouts */ + if (need_pack && ((error = git_packbuilder__prepare(push->pb))) < 0) + goto done; + + if ((error = git_smart__get_push_stream(t, &packbuilder_payload.stream)) < 0 || + (error = gen_pktline(&pktline, push)) < 0 || + (error = packbuilder_payload.stream->write(packbuilder_payload.stream, git_str_cstr(&pktline), git_str_len(&pktline))) < 0) + goto done; + + if (need_pack && + (error = git_packbuilder_foreach(push->pb, &stream_thunk, &packbuilder_payload)) < 0) + goto done; + + /* If we sent nothing or the server doesn't support report-status, then + * we consider the pack to have been unpacked successfully */ + if (!push->specs.length || !push->report_status) + push->unpack_ok = 1; + else if ((error = parse_report(t, push)) < 0) + goto done; + + /* If progress is being reported write the final report */ + if (cbs && cbs->push_transfer_progress) { + error = cbs->push_transfer_progress( + push->pb->nr_written, + push->pb->nr_objects, + packbuilder_payload.last_bytes, + cbs->payload); + + if (error < 0) + goto done; + } + + if (push->status.length) { + error = update_refs_from_report(&t->refs, &push->specs, &push->status); + if (error < 0) + goto done; + + error = git_smart__update_heads(t, NULL); + } + +done: + git_str_dispose(&pktline); + return error; +} diff --git a/src/libgit2/transports/ssh.c b/src/libgit2/transports/ssh.c new file mode 100644 index 0000000..de63d45 --- /dev/null +++ b/src/libgit2/transports/ssh.c @@ -0,0 +1,1147 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "ssh.h" + +#ifdef GIT_SSH +#include +#endif + +#include "runtime.h" +#include "net.h" +#include "smart.h" +#include "streams/socket.h" +#include "sysdir.h" + +#include "git2/credential.h" +#include "git2/sys/credential.h" + +#ifdef GIT_SSH + +#define OWNING_SUBTRANSPORT(s) ((ssh_subtransport *)(s)->parent.subtransport) + +static const char cmd_uploadpack[] = "git-upload-pack"; +static const char cmd_receivepack[] = "git-receive-pack"; + +typedef struct { + git_smart_subtransport_stream parent; + git_stream *io; + LIBSSH2_SESSION *session; + LIBSSH2_CHANNEL *channel; + const char *cmd; + git_net_url url; + unsigned sent_command : 1; +} ssh_stream; + +typedef struct { + git_smart_subtransport parent; + transport_smart *owner; + ssh_stream *current_stream; + git_credential *cred; + char *cmd_uploadpack; + char *cmd_receivepack; +} ssh_subtransport; + +static int list_auth_methods(int *out, LIBSSH2_SESSION *session, const char *username); + +static void ssh_error(LIBSSH2_SESSION *session, const char *errmsg) +{ + char *ssherr; + libssh2_session_last_error(session, &ssherr, NULL, 0); + + git_error_set(GIT_ERROR_SSH, "%s: %s", errmsg, ssherr); +} + +/* + * Create a git protocol request. + * + * For example: git-upload-pack '/libgit2/libgit2' + */ +static int gen_proto(git_str *request, const char *cmd, git_net_url *url) +{ + const char *repo; + + repo = url->path; + + if (repo && repo[0] == '/' && repo[1] == '~') + repo++; + + if (!repo || !repo[0]) { + git_error_set(GIT_ERROR_NET, "malformed git protocol URL"); + return -1; + } + + git_str_puts(request, cmd); + git_str_puts(request, " '"); + git_str_puts(request, repo); + git_str_puts(request, "'"); + + if (git_str_oom(request)) + return -1; + + return 0; +} + +static int send_command(ssh_stream *s) +{ + int error; + git_str request = GIT_STR_INIT; + + error = gen_proto(&request, s->cmd, &s->url); + if (error < 0) + goto cleanup; + + error = libssh2_channel_exec(s->channel, request.ptr); + if (error < LIBSSH2_ERROR_NONE) { + ssh_error(s->session, "SSH could not execute request"); + goto cleanup; + } + + s->sent_command = 1; + +cleanup: + git_str_dispose(&request); + return error; +} + +static int ssh_stream_read( + git_smart_subtransport_stream *stream, + char *buffer, + size_t buf_size, + size_t *bytes_read) +{ + int rc; + ssh_stream *s = GIT_CONTAINER_OF(stream, ssh_stream, parent); + + *bytes_read = 0; + + if (!s->sent_command && send_command(s) < 0) + return -1; + + if ((rc = libssh2_channel_read(s->channel, buffer, buf_size)) < LIBSSH2_ERROR_NONE) { + ssh_error(s->session, "SSH could not read data"); + return -1; + } + + /* + * If we can't get anything out of stdout, it's typically a + * not-found error, so read from stderr and signal EOF on + * stderr. + */ + if (rc == 0) { + if ((rc = libssh2_channel_read_stderr(s->channel, buffer, buf_size)) > 0) { + git_error_set(GIT_ERROR_SSH, "%*s", rc, buffer); + return GIT_EEOF; + } else if (rc < LIBSSH2_ERROR_NONE) { + ssh_error(s->session, "SSH could not read stderr"); + return -1; + } + } + + + *bytes_read = rc; + + return 0; +} + +static int ssh_stream_write( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + ssh_stream *s = GIT_CONTAINER_OF(stream, ssh_stream, parent); + size_t off = 0; + ssize_t ret = 0; + + if (!s->sent_command && send_command(s) < 0) + return -1; + + do { + ret = libssh2_channel_write(s->channel, buffer + off, len - off); + if (ret < 0) + break; + + off += ret; + + } while (off < len); + + if (ret < 0) { + ssh_error(s->session, "SSH could not write data"); + return -1; + } + + return 0; +} + +static void ssh_stream_free(git_smart_subtransport_stream *stream) +{ + ssh_stream *s = GIT_CONTAINER_OF(stream, ssh_stream, parent); + ssh_subtransport *t; + + if (!stream) + return; + + t = OWNING_SUBTRANSPORT(s); + t->current_stream = NULL; + + if (s->channel) { + libssh2_channel_close(s->channel); + libssh2_channel_free(s->channel); + s->channel = NULL; + } + + if (s->session) { + libssh2_session_disconnect(s->session, "closing transport"); + libssh2_session_free(s->session); + s->session = NULL; + } + + if (s->io) { + git_stream_close(s->io); + git_stream_free(s->io); + s->io = NULL; + } + + git_net_url_dispose(&s->url); + git__free(s); +} + +static int ssh_stream_alloc( + ssh_subtransport *t, + const char *cmd, + git_smart_subtransport_stream **stream) +{ + ssh_stream *s; + + GIT_ASSERT_ARG(stream); + + s = git__calloc(sizeof(ssh_stream), 1); + GIT_ERROR_CHECK_ALLOC(s); + + s->parent.subtransport = &t->parent; + s->parent.read = ssh_stream_read; + s->parent.write = ssh_stream_write; + s->parent.free = ssh_stream_free; + + s->cmd = cmd; + + *stream = &s->parent; + return 0; +} + +static int ssh_agent_auth(LIBSSH2_SESSION *session, git_credential_ssh_key *c) { + int rc = LIBSSH2_ERROR_NONE; + + struct libssh2_agent_publickey *curr, *prev = NULL; + + LIBSSH2_AGENT *agent = libssh2_agent_init(session); + + if (agent == NULL) + return -1; + + rc = libssh2_agent_connect(agent); + + if (rc != LIBSSH2_ERROR_NONE) { + rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED; + goto shutdown; + } + + rc = libssh2_agent_list_identities(agent); + + if (rc != LIBSSH2_ERROR_NONE) + goto shutdown; + + while (1) { + rc = libssh2_agent_get_identity(agent, &curr, prev); + + if (rc < 0) + goto shutdown; + + /* rc is set to 1 whenever the ssh agent ran out of keys to check. + * Set the error code to authentication failure rather than erroring + * out with an untranslatable error code. + */ + if (rc == 1) { + rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED; + goto shutdown; + } + + rc = libssh2_agent_userauth(agent, c->username, curr); + + if (rc == 0) + break; + + prev = curr; + } + +shutdown: + + if (rc != LIBSSH2_ERROR_NONE) + ssh_error(session, "error authenticating"); + + libssh2_agent_disconnect(agent); + libssh2_agent_free(agent); + + return rc; +} + +static int _git_ssh_authenticate_session( + LIBSSH2_SESSION *session, + git_credential *cred) +{ + int rc; + + do { + git_error_clear(); + switch (cred->credtype) { + case GIT_CREDENTIAL_USERPASS_PLAINTEXT: { + git_credential_userpass_plaintext *c = (git_credential_userpass_plaintext *)cred; + rc = libssh2_userauth_password(session, c->username, c->password); + break; + } + case GIT_CREDENTIAL_SSH_KEY: { + git_credential_ssh_key *c = (git_credential_ssh_key *)cred; + + if (c->privatekey) + rc = libssh2_userauth_publickey_fromfile( + session, c->username, c->publickey, + c->privatekey, c->passphrase); + else + rc = ssh_agent_auth(session, c); + + break; + } + case GIT_CREDENTIAL_SSH_CUSTOM: { + git_credential_ssh_custom *c = (git_credential_ssh_custom *)cred; + + rc = libssh2_userauth_publickey( + session, c->username, (const unsigned char *)c->publickey, + c->publickey_len, c->sign_callback, &c->payload); + break; + } + case GIT_CREDENTIAL_SSH_INTERACTIVE: { + void **abstract = libssh2_session_abstract(session); + git_credential_ssh_interactive *c = (git_credential_ssh_interactive *)cred; + + /* ideally, we should be able to set this by calling + * libssh2_session_init_ex() instead of libssh2_session_init(). + * libssh2's API is inconsistent here i.e. libssh2_userauth_publickey() + * allows you to pass the `abstract` as part of the call, whereas + * libssh2_userauth_keyboard_interactive() does not! + * + * The only way to set the `abstract` pointer is by calling + * libssh2_session_abstract(), which will replace the existing + * pointer as is done below. This is safe for now (at time of writing), + * but may not be valid in future. + */ + *abstract = c->payload; + + rc = libssh2_userauth_keyboard_interactive( + session, c->username, c->prompt_callback); + break; + } +#ifdef GIT_SSH_MEMORY_CREDENTIALS + case GIT_CREDENTIAL_SSH_MEMORY: { + git_credential_ssh_key *c = (git_credential_ssh_key *)cred; + + GIT_ASSERT(c->username); + GIT_ASSERT(c->privatekey); + + rc = libssh2_userauth_publickey_frommemory( + session, + c->username, + strlen(c->username), + c->publickey, + c->publickey ? strlen(c->publickey) : 0, + c->privatekey, + strlen(c->privatekey), + c->passphrase); + break; + } +#endif + default: + rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED; + } + } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); + + if (rc == LIBSSH2_ERROR_PASSWORD_EXPIRED || + rc == LIBSSH2_ERROR_AUTHENTICATION_FAILED || + rc == LIBSSH2_ERROR_PUBLICKEY_UNVERIFIED) + return GIT_EAUTH; + + if (rc != LIBSSH2_ERROR_NONE) { + if (!git_error_last()) + ssh_error(session, "Failed to authenticate SSH session"); + return -1; + } + + return 0; +} + +static int request_creds(git_credential **out, ssh_subtransport *t, const char *user, int auth_methods) +{ + int error, no_callback = 0; + git_credential *cred = NULL; + + if (!t->owner->connect_opts.callbacks.credentials) { + no_callback = 1; + } else { + error = t->owner->connect_opts.callbacks.credentials( + &cred, + t->owner->url, + user, + auth_methods, + t->owner->connect_opts.callbacks.payload); + + if (error == GIT_PASSTHROUGH) { + no_callback = 1; + } else if (error < 0) { + return error; + } else if (!cred) { + git_error_set(GIT_ERROR_SSH, "callback failed to initialize SSH credentials"); + return -1; + } + } + + if (no_callback) { + git_error_set(GIT_ERROR_SSH, "authentication required but no callback set"); + return GIT_EAUTH; + } + + if (!(cred->credtype & auth_methods)) { + cred->free(cred); + git_error_set(GIT_ERROR_SSH, "authentication callback returned unsupported credentials type"); + return GIT_EAUTH; + } + + *out = cred; + + return 0; +} + +#define SSH_DIR ".ssh" +#define KNOWN_HOSTS_FILE "known_hosts" + +/* + * Load the known_hosts file. + * + * Returns success but leaves the output NULL if we couldn't find the file. + */ +static int load_known_hosts(LIBSSH2_KNOWNHOSTS **hosts, LIBSSH2_SESSION *session) +{ + git_str path = GIT_STR_INIT, sshdir = GIT_STR_INIT; + LIBSSH2_KNOWNHOSTS *known_hosts = NULL; + int error; + + GIT_ASSERT_ARG(hosts); + + if ((error = git_sysdir_expand_homedir_file(&sshdir, SSH_DIR)) < 0 || + (error = git_str_joinpath(&path, git_str_cstr(&sshdir), KNOWN_HOSTS_FILE)) < 0) + goto out; + + if ((known_hosts = libssh2_knownhost_init(session)) == NULL) { + ssh_error(session, "error initializing known hosts"); + error = -1; + goto out; + } + + /* + * Try to read the file and consider not finding it as not trusting the + * host rather than an error. + */ + error = libssh2_knownhost_readfile(known_hosts, git_str_cstr(&path), LIBSSH2_KNOWNHOST_FILE_OPENSSH); + if (error == LIBSSH2_ERROR_FILE) + error = 0; + if (error < 0) + ssh_error(session, "error reading known_hosts"); + +out: + *hosts = known_hosts; + + git_str_dispose(&sshdir); + git_str_dispose(&path); + + return error; +} + +static void add_hostkey_pref_if_avail( + LIBSSH2_KNOWNHOSTS *known_hosts, + const char *hostname, + int port, + git_str *prefs, + int type, + const char *type_name) +{ + struct libssh2_knownhost *host = NULL; + const char key = '\0'; + int mask = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW | type; + int error; + + error = libssh2_knownhost_checkp(known_hosts, hostname, port, &key, 1, mask, &host); + if (error == LIBSSH2_KNOWNHOST_CHECK_MISMATCH) { + if (git_str_len(prefs) > 0) { + git_str_putc(prefs, ','); + } + git_str_puts(prefs, type_name); + } +} + +/* + * We figure out what kind of key we want to ask the remote for by trying to + * look it up with a nonsense key and using that mismatch to figure out what key + * we do have stored for the host. + * + * Populates prefs with the string to pass to libssh2_session_method_pref. + */ +static void find_hostkey_preference( + LIBSSH2_KNOWNHOSTS *known_hosts, + const char *hostname, + int port, + git_str *prefs) +{ + /* + * The order here is important as it indicates the priority of what will + * be preferred. + */ +#ifdef LIBSSH2_KNOWNHOST_KEY_ED25519 + add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ED25519, "ssh-ed25519"); +#endif +#ifdef LIBSSH2_KNOWNHOST_KEY_ECDSA_256 + add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ECDSA_256, "ecdsa-sha2-nistp256"); + add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ECDSA_384, "ecdsa-sha2-nistp384"); + add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_ECDSA_521, "ecdsa-sha2-nistp521"); +#endif + add_hostkey_pref_if_avail(known_hosts, hostname, port, prefs, LIBSSH2_KNOWNHOST_KEY_SSHRSA, "ssh-rsa"); +} + +static int _git_ssh_session_create( + LIBSSH2_SESSION **session, + LIBSSH2_KNOWNHOSTS **hosts, + const char *hostname, + int port, + git_stream *io) +{ + git_socket_stream *socket = GIT_CONTAINER_OF(io, git_socket_stream, parent); + LIBSSH2_SESSION *s; + LIBSSH2_KNOWNHOSTS *known_hosts; + git_str prefs = GIT_STR_INIT; + int rc = 0; + + GIT_ASSERT_ARG(session); + GIT_ASSERT_ARG(hosts); + + s = libssh2_session_init(); + if (!s) { + git_error_set(GIT_ERROR_NET, "failed to initialize SSH session"); + return -1; + } + + if ((rc = load_known_hosts(&known_hosts, s)) < 0) { + ssh_error(s, "error loading known_hosts"); + libssh2_session_free(s); + return -1; + } + + find_hostkey_preference(known_hosts, hostname, port, &prefs); + if (git_str_len(&prefs) > 0) { + do { + rc = libssh2_session_method_pref(s, LIBSSH2_METHOD_HOSTKEY, git_str_cstr(&prefs)); + } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); + if (rc != LIBSSH2_ERROR_NONE) { + ssh_error(s, "failed to set hostkey preference"); + goto on_error; + } + } + git_str_dispose(&prefs); + + do { + rc = libssh2_session_handshake(s, socket->s); + } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); + + if (rc != LIBSSH2_ERROR_NONE) { + ssh_error(s, "failed to start SSH session"); + goto on_error; + } + + libssh2_session_set_blocking(s, 1); + + *session = s; + *hosts = known_hosts; + + return 0; + +on_error: + libssh2_knownhost_free(known_hosts); + libssh2_session_free(s); + return -1; +} + + +/* + * Returns the typemask argument to pass to libssh2_knownhost_check{,p} based on + * the type of key that libssh2_session_hostkey returns. + */ +static int fingerprint_type_mask(int keytype) +{ + int mask = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW; + return mask; + + switch (keytype) { + case LIBSSH2_HOSTKEY_TYPE_RSA: + mask |= LIBSSH2_KNOWNHOST_KEY_SSHRSA; + break; + case LIBSSH2_HOSTKEY_TYPE_DSS: + mask |= LIBSSH2_KNOWNHOST_KEY_SSHDSS; + break; +#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_256 + case LIBSSH2_HOSTKEY_TYPE_ECDSA_256: + mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_256; + break; + case LIBSSH2_HOSTKEY_TYPE_ECDSA_384: + mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_384; + break; + case LIBSSH2_HOSTKEY_TYPE_ECDSA_521: + mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_521; + break; +#endif +#ifdef LIBSSH2_HOSTKEY_TYPE_ED25519 + case LIBSSH2_HOSTKEY_TYPE_ED25519: + mask |= LIBSSH2_KNOWNHOST_KEY_ED25519; + break; +#endif + } + + return mask; +} + +/* + * Check the host against the user's known_hosts file. + * + * Returns 1/0 for valid/''not-valid or <0 for an error + */ +static int check_against_known_hosts( + LIBSSH2_SESSION *session, + LIBSSH2_KNOWNHOSTS *known_hosts, + const char *hostname, + int port, + const char *key, + size_t key_len, + int key_type) +{ + int check, typemask, ret = 0; + struct libssh2_knownhost *host = NULL; + + if (known_hosts == NULL) + return 0; + + typemask = fingerprint_type_mask(key_type); + check = libssh2_knownhost_checkp(known_hosts, hostname, port, key, key_len, typemask, &host); + if (check == LIBSSH2_KNOWNHOST_CHECK_FAILURE) { + ssh_error(session, "error checking for known host"); + return -1; + } + + ret = check == LIBSSH2_KNOWNHOST_CHECK_MATCH ? 1 : 0; + + return ret; +} + +/* + * Perform the check for the session's certificate against known hosts if + * possible and then ask the user if they have a callback. + * + * Returns 1/0 for valid/not-valid or <0 for an error + */ +static int check_certificate( + LIBSSH2_SESSION *session, + LIBSSH2_KNOWNHOSTS *known_hosts, + git_transport_certificate_check_cb check_cb, + void *check_cb_payload, + const char *host, + int port) +{ + git_cert_hostkey cert = {{ 0 }}; + const char *key; + size_t cert_len; + int cert_type, cert_valid = 0, error = 0; + + if ((key = libssh2_session_hostkey(session, &cert_len, &cert_type)) == NULL) { + ssh_error(session, "failed to retrieve hostkey"); + return -1; + } + + if ((cert_valid = check_against_known_hosts(session, known_hosts, host, port, key, cert_len, cert_type)) < 0) + return -1; + + cert.parent.cert_type = GIT_CERT_HOSTKEY_LIBSSH2; + if (key != NULL) { + cert.type |= GIT_CERT_SSH_RAW; + cert.hostkey = key; + cert.hostkey_len = cert_len; + switch (cert_type) { + case LIBSSH2_HOSTKEY_TYPE_RSA: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_RSA; + break; + case LIBSSH2_HOSTKEY_TYPE_DSS: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_DSS; + break; + +#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_256 + case LIBSSH2_HOSTKEY_TYPE_ECDSA_256: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_256; + break; + case LIBSSH2_HOSTKEY_TYPE_ECDSA_384: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_384; + break; + case LIBSSH2_KNOWNHOST_KEY_ECDSA_521: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_521; + break; +#endif + +#ifdef LIBSSH2_HOSTKEY_TYPE_ED25519 + case LIBSSH2_HOSTKEY_TYPE_ED25519: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ED25519; + break; +#endif + default: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_UNKNOWN; + } + } + +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA256); + if (key != NULL) { + cert.type |= GIT_CERT_SSH_SHA256; + memcpy(&cert.hash_sha256, key, 32); + } +#endif + + key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1); + if (key != NULL) { + cert.type |= GIT_CERT_SSH_SHA1; + memcpy(&cert.hash_sha1, key, 20); + } + + key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_MD5); + if (key != NULL) { + cert.type |= GIT_CERT_SSH_MD5; + memcpy(&cert.hash_md5, key, 16); + } + + if (cert.type == 0) { + git_error_set(GIT_ERROR_SSH, "unable to get the host key"); + return -1; + } + + git_error_clear(); + error = 0; + if (!cert_valid) { + git_error_set(GIT_ERROR_SSH, "invalid or unknown remote ssh hostkey"); + error = GIT_ECERTIFICATE; + } + + if (check_cb != NULL) { + git_cert_hostkey *cert_ptr = &cert; + git_error_state previous_error = {0}; + + git_error_state_capture(&previous_error, error); + error = check_cb((git_cert *) cert_ptr, cert_valid, host, check_cb_payload); + if (error == GIT_PASSTHROUGH) { + error = git_error_state_restore(&previous_error); + } else if (error < 0 && !git_error_last()) { + git_error_set(GIT_ERROR_NET, "unknown remote host key"); + } + + git_error_state_free(&previous_error); + } + + return error; +} + +#define SSH_DEFAULT_PORT "22" + +static int _git_ssh_setup_conn( + ssh_subtransport *t, + const char *url, + const char *cmd, + git_smart_subtransport_stream **stream) +{ + int auth_methods, error = 0, port; + ssh_stream *s; + git_credential *cred = NULL; + LIBSSH2_SESSION *session=NULL; + LIBSSH2_CHANNEL *channel=NULL; + LIBSSH2_KNOWNHOSTS *known_hosts = NULL; + + t->current_stream = NULL; + + *stream = NULL; + if (ssh_stream_alloc(t, cmd, stream) < 0) + return -1; + + s = (ssh_stream *)*stream; + s->session = NULL; + s->channel = NULL; + + if ((error = git_net_url_parse_standard_or_scp(&s->url, url)) < 0 || + (error = git_socket_stream_new(&s->io, s->url.host, s->url.port)) < 0 || + (error = git_stream_connect(s->io)) < 0) + goto done; + + /* + * Try to parse the port as a number, if we can't then fall back to + * default. It would be nice if we could get the port that was resolved + * as part of the stream connection, but that's not something that's + * exposed. + */ + if (git__strntol32(&port, s->url.port, strlen(s->url.port), NULL, 10) < 0) { + git_error_set(GIT_ERROR_NET, "invalid port to ssh: %s", s->url.port); + error = -1; + goto done; + } + + if ((error = _git_ssh_session_create(&session, &known_hosts, s->url.host, port, s->io)) < 0) + goto done; + + if ((error = check_certificate(session, known_hosts, t->owner->connect_opts.callbacks.certificate_check, t->owner->connect_opts.callbacks.payload, s->url.host, port)) < 0) + goto done; + + /* we need the username to ask for auth methods */ + if (!s->url.username) { + if ((error = request_creds(&cred, t, NULL, GIT_CREDENTIAL_USERNAME)) < 0) + goto done; + + s->url.username = git__strdup(((git_credential_username *) cred)->username); + cred->free(cred); + cred = NULL; + if (!s->url.username) + goto done; + } else if (s->url.username && s->url.password) { + if ((error = git_credential_userpass_plaintext_new(&cred, s->url.username, s->url.password)) < 0) + goto done; + } + + if ((error = list_auth_methods(&auth_methods, session, s->url.username)) < 0) + goto done; + + error = GIT_EAUTH; + /* if we already have something to try */ + if (cred && auth_methods & cred->credtype) + error = _git_ssh_authenticate_session(session, cred); + + while (error == GIT_EAUTH) { + if (cred) { + cred->free(cred); + cred = NULL; + } + + if ((error = request_creds(&cred, t, s->url.username, auth_methods)) < 0) + goto done; + + if (strcmp(s->url.username, git_credential_get_username(cred))) { + git_error_set(GIT_ERROR_SSH, "username does not match previous request"); + error = -1; + goto done; + } + + error = _git_ssh_authenticate_session(session, cred); + + if (error == GIT_EAUTH) { + /* refresh auth methods */ + if ((error = list_auth_methods(&auth_methods, session, s->url.username)) < 0) + goto done; + else + error = GIT_EAUTH; + } + } + + if (error < 0) + goto done; + + channel = libssh2_channel_open_session(session); + if (!channel) { + error = -1; + ssh_error(session, "Failed to open SSH channel"); + goto done; + } + + libssh2_channel_set_blocking(channel, 1); + + s->session = session; + s->channel = channel; + + t->current_stream = s; + +done: + if (known_hosts) + libssh2_knownhost_free(known_hosts); + + if (error < 0) { + ssh_stream_free(*stream); + + if (session) + libssh2_session_free(session); + } + + if (cred) + cred->free(cred); + + return error; +} + +static int ssh_uploadpack_ls( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + const char *cmd = t->cmd_uploadpack ? t->cmd_uploadpack : cmd_uploadpack; + + return _git_ssh_setup_conn(t, url, cmd, stream); +} + +static int ssh_uploadpack( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + GIT_UNUSED(url); + + if (t->current_stream) { + *stream = &t->current_stream->parent; + return 0; + } + + git_error_set(GIT_ERROR_NET, "must call UPLOADPACK_LS before UPLOADPACK"); + return -1; +} + +static int ssh_receivepack_ls( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + const char *cmd = t->cmd_receivepack ? t->cmd_receivepack : cmd_receivepack; + + + return _git_ssh_setup_conn(t, url, cmd, stream); +} + +static int ssh_receivepack( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + GIT_UNUSED(url); + + if (t->current_stream) { + *stream = &t->current_stream->parent; + return 0; + } + + git_error_set(GIT_ERROR_NET, "must call RECEIVEPACK_LS before RECEIVEPACK"); + return -1; +} + +static int _ssh_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) +{ + ssh_subtransport *t = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent); + + switch (action) { + case GIT_SERVICE_UPLOADPACK_LS: + return ssh_uploadpack_ls(t, url, stream); + + case GIT_SERVICE_UPLOADPACK: + return ssh_uploadpack(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK_LS: + return ssh_receivepack_ls(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK: + return ssh_receivepack(t, url, stream); + } + + *stream = NULL; + return -1; +} + +static int _ssh_close(git_smart_subtransport *subtransport) +{ + ssh_subtransport *t = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent); + + GIT_ASSERT(!t->current_stream); + + GIT_UNUSED(t); + + return 0; +} + +static void _ssh_free(git_smart_subtransport *subtransport) +{ + ssh_subtransport *t = GIT_CONTAINER_OF(subtransport, ssh_subtransport, parent); + + git__free(t->cmd_uploadpack); + git__free(t->cmd_receivepack); + git__free(t); +} + +#define SSH_AUTH_PUBLICKEY "publickey" +#define SSH_AUTH_PASSWORD "password" +#define SSH_AUTH_KEYBOARD_INTERACTIVE "keyboard-interactive" + +static int list_auth_methods(int *out, LIBSSH2_SESSION *session, const char *username) +{ + const char *list, *ptr; + + *out = 0; + + list = libssh2_userauth_list(session, username, strlen(username)); + + /* either error, or the remote accepts NONE auth, which is bizarre, let's punt */ + if (list == NULL && !libssh2_userauth_authenticated(session)) { + ssh_error(session, "remote rejected authentication"); + return GIT_EAUTH; + } + + ptr = list; + while (ptr) { + if (*ptr == ',') + ptr++; + + if (!git__prefixcmp(ptr, SSH_AUTH_PUBLICKEY)) { + *out |= GIT_CREDENTIAL_SSH_KEY; + *out |= GIT_CREDENTIAL_SSH_CUSTOM; +#ifdef GIT_SSH_MEMORY_CREDENTIALS + *out |= GIT_CREDENTIAL_SSH_MEMORY; +#endif + ptr += strlen(SSH_AUTH_PUBLICKEY); + continue; + } + + if (!git__prefixcmp(ptr, SSH_AUTH_PASSWORD)) { + *out |= GIT_CREDENTIAL_USERPASS_PLAINTEXT; + ptr += strlen(SSH_AUTH_PASSWORD); + continue; + } + + if (!git__prefixcmp(ptr, SSH_AUTH_KEYBOARD_INTERACTIVE)) { + *out |= GIT_CREDENTIAL_SSH_INTERACTIVE; + ptr += strlen(SSH_AUTH_KEYBOARD_INTERACTIVE); + continue; + } + + /* Skip it if we don't know it */ + ptr = strchr(ptr, ','); + } + + return 0; +} +#endif + +int git_smart_subtransport_ssh( + git_smart_subtransport **out, git_transport *owner, void *param) +{ +#ifdef GIT_SSH + ssh_subtransport *t; + + GIT_ASSERT_ARG(out); + + GIT_UNUSED(param); + + t = git__calloc(sizeof(ssh_subtransport), 1); + GIT_ERROR_CHECK_ALLOC(t); + + t->owner = (transport_smart *)owner; + t->parent.action = _ssh_action; + t->parent.close = _ssh_close; + t->parent.free = _ssh_free; + + *out = (git_smart_subtransport *) t; + return 0; +#else + GIT_UNUSED(owner); + GIT_UNUSED(param); + + GIT_ASSERT_ARG(out); + *out = NULL; + + git_error_set(GIT_ERROR_INVALID, "cannot create SSH transport. Library was built without SSH support"); + return -1; +#endif +} + +int git_transport_ssh_with_paths(git_transport **out, git_remote *owner, void *payload) +{ +#ifdef GIT_SSH + git_strarray *paths = (git_strarray *) payload; + git_transport *transport; + transport_smart *smart; + ssh_subtransport *t; + int error; + git_smart_subtransport_definition ssh_definition = { + git_smart_subtransport_ssh, + 0, /* no RPC */ + NULL, + }; + + if (paths->count != 2) { + git_error_set(GIT_ERROR_SSH, "invalid ssh paths, must be two strings"); + return GIT_EINVALIDSPEC; + } + + if ((error = git_transport_smart(&transport, owner, &ssh_definition)) < 0) + return error; + + smart = (transport_smart *) transport; + t = (ssh_subtransport *) smart->wrapped; + + t->cmd_uploadpack = git__strdup(paths->strings[0]); + GIT_ERROR_CHECK_ALLOC(t->cmd_uploadpack); + t->cmd_receivepack = git__strdup(paths->strings[1]); + GIT_ERROR_CHECK_ALLOC(t->cmd_receivepack); + + *out = transport; + return 0; +#else + GIT_UNUSED(owner); + GIT_UNUSED(payload); + + GIT_ASSERT_ARG(out); + *out = NULL; + + git_error_set(GIT_ERROR_INVALID, "cannot create SSH transport. Library was built without SSH support"); + return -1; +#endif +} + +#ifdef GIT_SSH +static void shutdown_ssh(void) +{ + libssh2_exit(); +} +#endif + +int git_transport_ssh_global_init(void) +{ +#ifdef GIT_SSH + if (libssh2_init(0) < 0) { + git_error_set(GIT_ERROR_SSH, "unable to initialize libssh2"); + return -1; + } + + return git_runtime_shutdown_register(shutdown_ssh); + +#else + + /* Nothing to initialize */ + return 0; + +#endif +} diff --git a/src/libgit2/transports/ssh.h b/src/libgit2/transports/ssh.h new file mode 100644 index 0000000..d3e741f --- /dev/null +++ b/src/libgit2/transports/ssh.h @@ -0,0 +1,14 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_transports_ssh_h__ +#define INCLUDE_transports_ssh_h__ + +#include "common.h" + +int git_transport_ssh_global_init(void); + +#endif diff --git a/src/libgit2/transports/winhttp.c b/src/libgit2/transports/winhttp.c new file mode 100644 index 0000000..ae572c5 --- /dev/null +++ b/src/libgit2/transports/winhttp.c @@ -0,0 +1,1690 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" + +#ifdef GIT_WINHTTP + +#include "git2.h" +#include "git2/transport.h" +#include "posix.h" +#include "str.h" +#include "smart.h" +#include "remote.h" +#include "repository.h" +#include "http.h" +#include "git2/sys/credential.h" + +#include +#include + +/* For IInternetSecurityManager zone check */ +#include +#include + +#define WIDEN2(s) L ## s +#define WIDEN(s) WIDEN2(s) + +#define MAX_CONTENT_TYPE_LEN 100 +#define WINHTTP_OPTION_PEERDIST_EXTENSION_STATE 109 +#define CACHED_POST_BODY_BUF_SIZE 4096 +#define UUID_LENGTH_CCH 32 +#define TIMEOUT_INFINITE -1 +#define DEFAULT_CONNECT_TIMEOUT 60000 +#ifndef WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH +#define WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH 0 +#endif + +#ifndef WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 +# define WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 0x00000200 +#endif + +#ifndef WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 +# define WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 0x00000800 +#endif + +#ifndef WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3 +# define WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3 0x00002000 +#endif + +#ifndef WINHTTP_NO_CLIENT_CERT_CONTEXT +# define WINHTTP_NO_CLIENT_CERT_CONTEXT NULL +#endif + +#ifndef HTTP_STATUS_PERMANENT_REDIRECT +# define HTTP_STATUS_PERMANENT_REDIRECT 308 +#endif + +#ifndef DWORD_MAX +# define DWORD_MAX 0xffffffff +#endif + +bool git_http__expect_continue = false; + +static const char *prefix_https = "https://"; +static const char *upload_pack_service = "upload-pack"; +static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack"; +static const char *upload_pack_service_url = "/git-upload-pack"; +static const char *receive_pack_service = "receive-pack"; +static const char *receive_pack_ls_service_url = "/info/refs?service=git-receive-pack"; +static const char *receive_pack_service_url = "/git-receive-pack"; +static const wchar_t *get_verb = L"GET"; +static const wchar_t *post_verb = L"POST"; +static const wchar_t *pragma_nocache = L"Pragma: no-cache"; +static const wchar_t *transfer_encoding = L"Transfer-Encoding: chunked"; +static const int no_check_cert_flags = SECURITY_FLAG_IGNORE_CERT_CN_INVALID | + SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | + SECURITY_FLAG_IGNORE_UNKNOWN_CA; + +#if defined(__MINGW32__) +static const CLSID CLSID_InternetSecurityManager_mingw = + { 0x7B8A2D94, 0x0AC9, 0x11D1, + { 0x89, 0x6C, 0x00, 0xC0, 0x4F, 0xB6, 0xBF, 0xC4 } }; +static const IID IID_IInternetSecurityManager_mingw = + { 0x79EAC9EE, 0xBAF9, 0x11CE, + { 0x8C, 0x82, 0x00, 0xAA, 0x00, 0x4B, 0xA9, 0x0B } }; + +# define CLSID_InternetSecurityManager CLSID_InternetSecurityManager_mingw +# define IID_IInternetSecurityManager IID_IInternetSecurityManager_mingw +#endif + +#define OWNING_SUBTRANSPORT(s) ((winhttp_subtransport *)(s)->parent.subtransport) + +typedef enum { + GIT_WINHTTP_AUTH_BASIC = 1, + GIT_WINHTTP_AUTH_NTLM = 2, + GIT_WINHTTP_AUTH_NEGOTIATE = 4, + GIT_WINHTTP_AUTH_DIGEST = 8 +} winhttp_authmechanism_t; + +typedef struct { + git_smart_subtransport_stream parent; + const char *service; + const char *service_url; + const wchar_t *verb; + HINTERNET request; + wchar_t *request_uri; + char *chunk_buffer; + unsigned chunk_buffer_len; + HANDLE post_body; + DWORD post_body_len; + unsigned sent_request : 1, + received_response : 1, + chunked : 1, + status_sending_request_reached: 1; +} winhttp_stream; + +typedef struct { + git_net_url url; + git_credential *cred; + int auth_mechanisms; + bool url_cred_presented; +} winhttp_server; + +typedef struct { + git_smart_subtransport parent; + transport_smart *owner; + + winhttp_server server; + winhttp_server proxy; + + HINTERNET session; + HINTERNET connection; +} winhttp_subtransport; + +static int apply_userpass_credentials(HINTERNET request, DWORD target, int mechanisms, git_credential *cred) +{ + git_credential_userpass_plaintext *c = (git_credential_userpass_plaintext *)cred; + wchar_t *user = NULL, *pass = NULL; + int user_len = 0, pass_len = 0, error = 0; + DWORD native_scheme; + + if (mechanisms & GIT_WINHTTP_AUTH_NEGOTIATE) { + native_scheme = WINHTTP_AUTH_SCHEME_NEGOTIATE; + } else if (mechanisms & GIT_WINHTTP_AUTH_NTLM) { + native_scheme = WINHTTP_AUTH_SCHEME_NTLM; + } else if (mechanisms & GIT_WINHTTP_AUTH_DIGEST) { + native_scheme = WINHTTP_AUTH_SCHEME_DIGEST; + } else if (mechanisms & GIT_WINHTTP_AUTH_BASIC) { + native_scheme = WINHTTP_AUTH_SCHEME_BASIC; + } else { + git_error_set(GIT_ERROR_HTTP, "invalid authentication scheme"); + error = GIT_EAUTH; + goto done; + } + + if ((error = user_len = git_utf8_to_16_alloc(&user, c->username)) < 0) + goto done; + + if ((error = pass_len = git_utf8_to_16_alloc(&pass, c->password)) < 0) + goto done; + + if (!WinHttpSetCredentials(request, target, native_scheme, user, pass, NULL)) { + git_error_set(GIT_ERROR_OS, "failed to set credentials"); + error = -1; + } + +done: + if (user_len > 0) + git__memzero(user, user_len * sizeof(wchar_t)); + + if (pass_len > 0) + git__memzero(pass, pass_len * sizeof(wchar_t)); + + git__free(user); + git__free(pass); + + return error; +} + +static int apply_default_credentials(HINTERNET request, DWORD target, int mechanisms) +{ + DWORD autologon_level = WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW; + DWORD native_scheme = 0; + + if ((mechanisms & GIT_WINHTTP_AUTH_NEGOTIATE) != 0) { + native_scheme = WINHTTP_AUTH_SCHEME_NEGOTIATE; + } else if ((mechanisms & GIT_WINHTTP_AUTH_NTLM) != 0) { + native_scheme = WINHTTP_AUTH_SCHEME_NTLM; + } else { + git_error_set(GIT_ERROR_HTTP, "invalid authentication scheme"); + return GIT_EAUTH; + } + + /* + * Autologon policy must be "low" to use default creds. + * This is safe as the user has explicitly requested it. + */ + if (!WinHttpSetOption(request, WINHTTP_OPTION_AUTOLOGON_POLICY, &autologon_level, sizeof(DWORD))) { + git_error_set(GIT_ERROR_OS, "could not configure logon policy"); + return -1; + } + + if (!WinHttpSetCredentials(request, target, native_scheme, NULL, NULL, NULL)) { + git_error_set(GIT_ERROR_OS, "could not configure credentials"); + return -1; + } + + return 0; +} + +static int acquire_url_cred( + git_credential **cred, + unsigned int allowed_types, + const char *username, + const char *password) +{ + if (allowed_types & GIT_CREDENTIAL_USERPASS_PLAINTEXT) + return git_credential_userpass_plaintext_new(cred, username, password); + + if ((allowed_types & GIT_CREDENTIAL_DEFAULT) && *username == '\0' && *password == '\0') + return git_credential_default_new(cred); + + return 1; +} + +static int acquire_fallback_cred( + git_credential **cred, + const char *url, + unsigned int allowed_types) +{ + int error = 1; + + /* If the target URI supports integrated Windows authentication + * as an authentication mechanism */ + if (GIT_CREDENTIAL_DEFAULT & allowed_types) { + wchar_t *wide_url; + HRESULT hCoInitResult; + + /* Convert URL to wide characters */ + if (git_utf8_to_16_alloc(&wide_url, url) < 0) { + git_error_set(GIT_ERROR_OS, "failed to convert string to wide form"); + return -1; + } + + hCoInitResult = CoInitializeEx(NULL, COINIT_MULTITHREADED); + + if (SUCCEEDED(hCoInitResult) || hCoInitResult == RPC_E_CHANGED_MODE) { + IInternetSecurityManager *pISM; + + /* And if the target URI is in the My Computer, Intranet, or Trusted zones */ + if (SUCCEEDED(CoCreateInstance(&CLSID_InternetSecurityManager, NULL, + CLSCTX_ALL, &IID_IInternetSecurityManager, (void **)&pISM))) { + DWORD dwZone; + + if (SUCCEEDED(pISM->lpVtbl->MapUrlToZone(pISM, wide_url, &dwZone, 0)) && + (URLZONE_LOCAL_MACHINE == dwZone || + URLZONE_INTRANET == dwZone || + URLZONE_TRUSTED == dwZone)) { + git_credential *existing = *cred; + + if (existing) + existing->free(existing); + + /* Then use default Windows credentials to authenticate this request */ + error = git_credential_default_new(cred); + } + + pISM->lpVtbl->Release(pISM); + } + + /* Only uninitialize if the call to CoInitializeEx was successful. */ + if (SUCCEEDED(hCoInitResult)) + CoUninitialize(); + } + + git__free(wide_url); + } + + return error; +} + +static int certificate_check(winhttp_stream *s, int valid) +{ + int error; + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + PCERT_CONTEXT cert_ctx; + DWORD cert_ctx_size = sizeof(cert_ctx); + git_cert_x509 cert; + + /* If there is no override, we should fail if WinHTTP doesn't think it's fine */ + if (t->owner->connect_opts.callbacks.certificate_check == NULL && !valid) { + if (!git_error_last()) + git_error_set(GIT_ERROR_HTTP, "unknown certificate check failure"); + + return GIT_ECERTIFICATE; + } + + if (t->owner->connect_opts.callbacks.certificate_check == NULL || git__strcmp(t->server.url.scheme, "https") != 0) + return 0; + + if (!WinHttpQueryOption(s->request, WINHTTP_OPTION_SERVER_CERT_CONTEXT, &cert_ctx, &cert_ctx_size)) { + git_error_set(GIT_ERROR_OS, "failed to get server certificate"); + return -1; + } + + git_error_clear(); + cert.parent.cert_type = GIT_CERT_X509; + cert.data = cert_ctx->pbCertEncoded; + cert.len = cert_ctx->cbCertEncoded; + error = t->owner->connect_opts.callbacks.certificate_check((git_cert *) &cert, valid, t->server.url.host, t->owner->connect_opts.callbacks.payload); + CertFreeCertificateContext(cert_ctx); + + if (error == GIT_PASSTHROUGH) + error = valid ? 0 : GIT_ECERTIFICATE; + + if (error < 0 && !git_error_last()) + git_error_set(GIT_ERROR_HTTP, "user cancelled certificate check"); + + return error; +} + +static void winhttp_stream_close(winhttp_stream *s) +{ + if (s->chunk_buffer) { + git__free(s->chunk_buffer); + s->chunk_buffer = NULL; + } + + if (s->post_body) { + CloseHandle(s->post_body); + s->post_body = NULL; + } + + if (s->request_uri) { + git__free(s->request_uri); + s->request_uri = NULL; + } + + if (s->request) { + WinHttpCloseHandle(s->request); + s->request = NULL; + } + + s->sent_request = 0; +} + +static int apply_credentials( + HINTERNET request, + git_net_url *url, + int target, + git_credential *creds, + int mechanisms) +{ + int error = 0; + + GIT_UNUSED(url); + + /* If we have creds, just apply them */ + if (creds && creds->credtype == GIT_CREDENTIAL_USERPASS_PLAINTEXT) + error = apply_userpass_credentials(request, target, mechanisms, creds); + else if (creds && creds->credtype == GIT_CREDENTIAL_DEFAULT) + error = apply_default_credentials(request, target, mechanisms); + + return error; +} + +static int winhttp_stream_connect(winhttp_stream *s) +{ + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + git_str buf = GIT_STR_INIT; + char *proxy_url = NULL; + wchar_t ct[MAX_CONTENT_TYPE_LEN]; + LPCWSTR types[] = { L"*/*", NULL }; + BOOL peerdist = FALSE; + int error = -1; + unsigned long disable_redirects = WINHTTP_DISABLE_REDIRECTS; + int default_timeout = TIMEOUT_INFINITE; + int default_connect_timeout = DEFAULT_CONNECT_TIMEOUT; + DWORD autologon_policy = WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH; + + const char *service_url = s->service_url; + size_t i; + const git_proxy_options *proxy_opts; + + /* If path already ends in /, remove the leading slash from service_url */ + if ((git__suffixcmp(t->server.url.path, "/") == 0) && (git__prefixcmp(service_url, "/") == 0)) + service_url++; + /* Prepare URL */ + git_str_printf(&buf, "%s%s", t->server.url.path, service_url); + + if (git_str_oom(&buf)) + return -1; + + /* Convert URL to wide characters */ + if (git_utf8_to_16_alloc(&s->request_uri, git_str_cstr(&buf)) < 0) { + git_error_set(GIT_ERROR_OS, "failed to convert string to wide form"); + goto on_error; + } + + /* Establish request */ + s->request = WinHttpOpenRequest( + t->connection, + s->verb, + s->request_uri, + NULL, + WINHTTP_NO_REFERER, + types, + git__strcmp(t->server.url.scheme, "https") == 0 ? WINHTTP_FLAG_SECURE : 0); + + if (!s->request) { + git_error_set(GIT_ERROR_OS, "failed to open request"); + goto on_error; + } + + /* Never attempt default credentials; we'll provide them explicitly. */ + if (!WinHttpSetOption(s->request, WINHTTP_OPTION_AUTOLOGON_POLICY, &autologon_policy, sizeof(DWORD))) + return -1; + + if (!WinHttpSetTimeouts(s->request, default_timeout, default_connect_timeout, default_timeout, default_timeout)) { + git_error_set(GIT_ERROR_OS, "failed to set timeouts for WinHTTP"); + goto on_error; + } + + proxy_opts = &t->owner->connect_opts.proxy_opts; + if (proxy_opts->type == GIT_PROXY_AUTO) { + /* Set proxy if necessary */ + if (git_remote__http_proxy(&proxy_url, t->owner->owner, &t->server.url) < 0) + goto on_error; + } + else if (proxy_opts->type == GIT_PROXY_SPECIFIED) { + proxy_url = git__strdup(proxy_opts->url); + GIT_ERROR_CHECK_ALLOC(proxy_url); + } + + if (proxy_url) { + git_str processed_url = GIT_STR_INIT; + WINHTTP_PROXY_INFO proxy_info; + wchar_t *proxy_wide; + + git_net_url_dispose(&t->proxy.url); + + if ((error = git_net_url_parse_http(&t->proxy.url, proxy_url)) < 0) + goto on_error; + + if (!git_net_url_valid(&t->proxy.url)) { + git_error_set(GIT_ERROR_HTTP, "invalid URL: '%s'", proxy_url); + error = -1; + goto on_error; + } + + git_str_puts(&processed_url, t->proxy.url.scheme); + git_str_PUTS(&processed_url, "://"); + + if (git_net_url_is_ipv6(&t->proxy.url)) + git_str_putc(&processed_url, '['); + + git_str_puts(&processed_url, t->proxy.url.host); + + if (git_net_url_is_ipv6(&t->proxy.url)) + git_str_putc(&processed_url, ']'); + + if (!git_net_url_is_default_port(&t->proxy.url)) + git_str_printf(&processed_url, ":%s", t->proxy.url.port); + + if (git_str_oom(&processed_url)) { + error = -1; + goto on_error; + } + + /* Convert URL to wide characters */ + error = git_utf8_to_16_alloc(&proxy_wide, processed_url.ptr); + git_str_dispose(&processed_url); + if (error < 0) + goto on_error; + + proxy_info.dwAccessType = WINHTTP_ACCESS_TYPE_NAMED_PROXY; + proxy_info.lpszProxy = proxy_wide; + proxy_info.lpszProxyBypass = NULL; + + if (!WinHttpSetOption(s->request, + WINHTTP_OPTION_PROXY, + &proxy_info, + sizeof(WINHTTP_PROXY_INFO))) { + git_error_set(GIT_ERROR_OS, "failed to set proxy"); + git__free(proxy_wide); + goto on_error; + } + + git__free(proxy_wide); + + if ((error = apply_credentials(s->request, &t->proxy.url, WINHTTP_AUTH_TARGET_PROXY, t->proxy.cred, t->proxy.auth_mechanisms)) < 0) + goto on_error; + } + + /* Disable WinHTTP redirects so we can handle them manually. Why, you ask? + * http://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/b2ff8879-ab9f-4218-8f09-16d25dff87ae + */ + if (!WinHttpSetOption(s->request, + WINHTTP_OPTION_DISABLE_FEATURE, + &disable_redirects, + sizeof(disable_redirects))) { + git_error_set(GIT_ERROR_OS, "failed to disable redirects"); + error = -1; + goto on_error; + } + + /* Strip unwanted headers (X-P2P-PeerDist, X-P2P-PeerDistEx) that WinHTTP + * adds itself. This option may not be supported by the underlying + * platform, so we do not error-check it */ + WinHttpSetOption(s->request, + WINHTTP_OPTION_PEERDIST_EXTENSION_STATE, + &peerdist, + sizeof(peerdist)); + + /* Send Pragma: no-cache header */ + if (!WinHttpAddRequestHeaders(s->request, pragma_nocache, (ULONG) -1L, WINHTTP_ADDREQ_FLAG_ADD)) { + git_error_set(GIT_ERROR_OS, "failed to add a header to the request"); + goto on_error; + } + + if (post_verb == s->verb) { + /* Send Content-Type and Accept headers -- only necessary on a POST */ + git_str_clear(&buf); + if (git_str_printf(&buf, + "Content-Type: application/x-git-%s-request", + s->service) < 0) + goto on_error; + + if (git_utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_str_cstr(&buf)) < 0) { + git_error_set(GIT_ERROR_OS, "failed to convert content-type to wide characters"); + goto on_error; + } + + if (!WinHttpAddRequestHeaders(s->request, ct, (ULONG)-1L, + WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) { + git_error_set(GIT_ERROR_OS, "failed to add a header to the request"); + goto on_error; + } + + git_str_clear(&buf); + if (git_str_printf(&buf, + "Accept: application/x-git-%s-result", + s->service) < 0) + goto on_error; + + if (git_utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_str_cstr(&buf)) < 0) { + git_error_set(GIT_ERROR_OS, "failed to convert accept header to wide characters"); + goto on_error; + } + + if (!WinHttpAddRequestHeaders(s->request, ct, (ULONG)-1L, + WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) { + git_error_set(GIT_ERROR_OS, "failed to add a header to the request"); + goto on_error; + } + } + + for (i = 0; i < t->owner->connect_opts.custom_headers.count; i++) { + if (t->owner->connect_opts.custom_headers.strings[i]) { + wchar_t *custom_header_wide = NULL; + + git_str_clear(&buf); + git_str_puts(&buf, t->owner->connect_opts.custom_headers.strings[i]); + + /* Convert header to wide characters */ + if ((error = git_utf8_to_16_alloc(&custom_header_wide, git_str_cstr(&buf))) < 0) + goto on_error; + + if (!WinHttpAddRequestHeaders(s->request, custom_header_wide, (ULONG)-1L, + WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) { + git_error_set(GIT_ERROR_OS, "failed to add a header to the request"); + git__free(custom_header_wide); + goto on_error; + } + + git__free(custom_header_wide); + } + } + + if ((error = apply_credentials(s->request, &t->server.url, WINHTTP_AUTH_TARGET_SERVER, t->server.cred, t->server.auth_mechanisms)) < 0) + goto on_error; + + /* We've done everything up to calling WinHttpSendRequest. */ + + error = 0; + +on_error: + if (error < 0) + winhttp_stream_close(s); + + git__free(proxy_url); + git_str_dispose(&buf); + return error; +} + +static int parse_unauthorized_response( + int *allowed_types, + int *allowed_mechanisms, + HINTERNET request) +{ + DWORD supported, first, target; + + *allowed_types = 0; + *allowed_mechanisms = 0; + + /* WinHttpQueryHeaders() must be called before WinHttpQueryAuthSchemes(). + * We can assume this was already done, since we know we are unauthorized. + */ + if (!WinHttpQueryAuthSchemes(request, &supported, &first, &target)) { + git_error_set(GIT_ERROR_OS, "failed to parse supported auth schemes"); + return GIT_EAUTH; + } + + if (WINHTTP_AUTH_SCHEME_NTLM & supported) { + *allowed_types |= GIT_CREDENTIAL_USERPASS_PLAINTEXT; + *allowed_types |= GIT_CREDENTIAL_DEFAULT; + *allowed_mechanisms |= GIT_WINHTTP_AUTH_NTLM; + } + + if (WINHTTP_AUTH_SCHEME_NEGOTIATE & supported) { + *allowed_types |= GIT_CREDENTIAL_DEFAULT; + *allowed_mechanisms |= GIT_WINHTTP_AUTH_NEGOTIATE; + } + + if (WINHTTP_AUTH_SCHEME_BASIC & supported) { + *allowed_types |= GIT_CREDENTIAL_USERPASS_PLAINTEXT; + *allowed_mechanisms |= GIT_WINHTTP_AUTH_BASIC; + } + + if (WINHTTP_AUTH_SCHEME_DIGEST & supported) { + *allowed_types |= GIT_CREDENTIAL_USERPASS_PLAINTEXT; + *allowed_mechanisms |= GIT_WINHTTP_AUTH_DIGEST; + } + + return 0; +} + +static int write_chunk(HINTERNET request, const char *buffer, size_t len) +{ + DWORD bytes_written; + git_str buf = GIT_STR_INIT; + + /* Chunk header */ + git_str_printf(&buf, "%"PRIXZ"\r\n", len); + + if (git_str_oom(&buf)) + return -1; + + if (!WinHttpWriteData(request, + git_str_cstr(&buf), (DWORD)git_str_len(&buf), + &bytes_written)) { + git_str_dispose(&buf); + git_error_set(GIT_ERROR_OS, "failed to write chunk header"); + return -1; + } + + git_str_dispose(&buf); + + /* Chunk body */ + if (!WinHttpWriteData(request, + buffer, (DWORD)len, + &bytes_written)) { + git_error_set(GIT_ERROR_OS, "failed to write chunk"); + return -1; + } + + /* Chunk footer */ + if (!WinHttpWriteData(request, + "\r\n", 2, + &bytes_written)) { + git_error_set(GIT_ERROR_OS, "failed to write chunk footer"); + return -1; + } + + return 0; +} + +static int winhttp_close_connection(winhttp_subtransport *t) +{ + int ret = 0; + + if (t->connection) { + if (!WinHttpCloseHandle(t->connection)) { + git_error_set(GIT_ERROR_OS, "unable to close connection"); + ret = -1; + } + + t->connection = NULL; + } + + if (t->session) { + if (!WinHttpCloseHandle(t->session)) { + git_error_set(GIT_ERROR_OS, "unable to close session"); + ret = -1; + } + + t->session = NULL; + } + + return ret; +} + +static void CALLBACK winhttp_status( + HINTERNET connection, + DWORD_PTR ctx, + DWORD code, + LPVOID info, + DWORD info_len) +{ + DWORD status; + + GIT_UNUSED(connection); + GIT_UNUSED(info_len); + + switch (code) { + case WINHTTP_CALLBACK_STATUS_SECURE_FAILURE: + status = *((DWORD *)info); + + if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_CN_INVALID)) + git_error_set(GIT_ERROR_HTTP, "SSL certificate issued for different common name"); + else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_DATE_INVALID)) + git_error_set(GIT_ERROR_HTTP, "SSL certificate has expired"); + else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CA)) + git_error_set(GIT_ERROR_HTTP, "SSL certificate signed by unknown CA"); + else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CERT)) + git_error_set(GIT_ERROR_HTTP, "SSL certificate is invalid"); + else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REV_FAILED)) + git_error_set(GIT_ERROR_HTTP, "certificate revocation check failed"); + else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REVOKED)) + git_error_set(GIT_ERROR_HTTP, "SSL certificate was revoked"); + else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_SECURITY_CHANNEL_ERROR)) + git_error_set(GIT_ERROR_HTTP, "security libraries could not be loaded"); + else + git_error_set(GIT_ERROR_HTTP, "unknown security error %lu", status); + + break; + + case WINHTTP_CALLBACK_STATUS_SENDING_REQUEST: + ((winhttp_stream *) ctx)->status_sending_request_reached = 1; + + break; + } +} + +static int winhttp_connect( + winhttp_subtransport *t) +{ + wchar_t *wide_host = NULL; + int32_t port; + wchar_t *wide_ua = NULL; + git_str ipv6 = GIT_STR_INIT, ua = GIT_STR_INIT; + const char *host; + int error = -1; + int default_timeout = TIMEOUT_INFINITE; + int default_connect_timeout = DEFAULT_CONNECT_TIMEOUT; + DWORD protocols = + WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 | + WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 | + WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 | + WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3; + + t->session = NULL; + t->connection = NULL; + + /* Prepare port */ + if (git__strntol32(&port, t->server.url.port, + strlen(t->server.url.port), NULL, 10) < 0) + goto on_error; + + /* IPv6? Add braces around the host. */ + if (git_net_url_is_ipv6(&t->server.url)) { + if (git_str_printf(&ipv6, "[%s]", t->server.url.host) < 0) + goto on_error; + + host = ipv6.ptr; + } else { + host = t->server.url.host; + } + + /* Prepare host */ + if (git_utf8_to_16_alloc(&wide_host, host) < 0) { + git_error_set(GIT_ERROR_OS, "unable to convert host to wide characters"); + goto on_error; + } + + + if (git_http__user_agent(&ua) < 0) + goto on_error; + + if (git_utf8_to_16_alloc(&wide_ua, git_str_cstr(&ua)) < 0) { + git_error_set(GIT_ERROR_OS, "unable to convert host to wide characters"); + goto on_error; + } + + /* Establish session */ + t->session = WinHttpOpen( + wide_ua, + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + + if (!t->session) { + git_error_set(GIT_ERROR_OS, "failed to init WinHTTP"); + goto on_error; + } + + /* + * Do a best-effort attempt to enable TLS 1.3 and 1.2 but allow this to + * fail; if TLS 1.2 or 1.3 support is not available for some reason, + * ignore the failure (it will keep the default protocols). + */ + if (WinHttpSetOption(t->session, + WINHTTP_OPTION_SECURE_PROTOCOLS, + &protocols, + sizeof(protocols)) == FALSE) { + protocols &= ~WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3; + WinHttpSetOption(t->session, + WINHTTP_OPTION_SECURE_PROTOCOLS, + &protocols, + sizeof(protocols)); + } + + if (!WinHttpSetTimeouts(t->session, default_timeout, default_connect_timeout, default_timeout, default_timeout)) { + git_error_set(GIT_ERROR_OS, "failed to set timeouts for WinHTTP"); + goto on_error; + } + + + /* Establish connection */ + t->connection = WinHttpConnect( + t->session, + wide_host, + (INTERNET_PORT) port, + 0); + + if (!t->connection) { + git_error_set(GIT_ERROR_OS, "failed to connect to host"); + goto on_error; + } + + if (WinHttpSetStatusCallback( + t->connection, + winhttp_status, + WINHTTP_CALLBACK_FLAG_SECURE_FAILURE | WINHTTP_CALLBACK_FLAG_SEND_REQUEST, + 0 + ) == WINHTTP_INVALID_STATUS_CALLBACK) { + git_error_set(GIT_ERROR_OS, "failed to set status callback"); + goto on_error; + } + + error = 0; + +on_error: + if (error < 0) + winhttp_close_connection(t); + + git_str_dispose(&ua); + git_str_dispose(&ipv6); + git__free(wide_host); + git__free(wide_ua); + + return error; +} + +static int do_send_request(winhttp_stream *s, size_t len, bool chunked) +{ + int attempts; + bool success; + + if (len > DWORD_MAX) { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return -1; + } + + for (attempts = 0; attempts < 5; attempts++) { + if (chunked) { + success = WinHttpSendRequest(s->request, + WINHTTP_NO_ADDITIONAL_HEADERS, 0, + WINHTTP_NO_REQUEST_DATA, 0, + WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH, (DWORD_PTR)s); + } else { + success = WinHttpSendRequest(s->request, + WINHTTP_NO_ADDITIONAL_HEADERS, 0, + WINHTTP_NO_REQUEST_DATA, 0, + (DWORD)len, (DWORD_PTR)s); + } + + if (success || GetLastError() != (DWORD)SEC_E_BUFFER_TOO_SMALL) + break; + } + + return success ? 0 : -1; +} + +static int send_request(winhttp_stream *s, size_t len, bool chunked) +{ + int request_failed = 1, error, attempts = 0; + DWORD ignore_flags, send_request_error; + + git_error_clear(); + + while (request_failed && attempts++ < 3) { + int cert_valid = 1; + int client_cert_requested = 0; + request_failed = 0; + if ((error = do_send_request(s, len, chunked)) < 0) { + send_request_error = GetLastError(); + request_failed = 1; + switch (send_request_error) { + case ERROR_WINHTTP_SECURE_FAILURE: + cert_valid = 0; + break; + case ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED: + client_cert_requested = 1; + break; + default: + git_error_set(GIT_ERROR_OS, "failed to send request"); + return -1; + } + } + + /* + * Only check the certificate if we were able to reach the sending request phase, or + * received a secure failure error. Otherwise, the server certificate won't be available + * since the request wasn't able to complete (e.g. proxy auth required) + */ + if (!cert_valid || + (!request_failed && s->status_sending_request_reached)) { + git_error_clear(); + if ((error = certificate_check(s, cert_valid)) < 0) { + if (!git_error_last()) + git_error_set(GIT_ERROR_OS, "user cancelled certificate check"); + + return error; + } + } + + /* if neither the request nor the certificate check returned errors, we're done */ + if (!request_failed) + return 0; + + if (!cert_valid) { + ignore_flags = no_check_cert_flags; + if (!WinHttpSetOption(s->request, WINHTTP_OPTION_SECURITY_FLAGS, &ignore_flags, sizeof(ignore_flags))) { + git_error_set(GIT_ERROR_OS, "failed to set security options"); + return -1; + } + } + + if (client_cert_requested) { + /* + * Client certificates are not supported, explicitly tell the server that + * (it's possible a client certificate was requested but is not required) + */ + if (!WinHttpSetOption(s->request, WINHTTP_OPTION_CLIENT_CERT_CONTEXT, WINHTTP_NO_CLIENT_CERT_CONTEXT, 0)) { + git_error_set(GIT_ERROR_OS, "failed to set client cert context"); + return -1; + } + } + } + + return error; +} + +static int acquire_credentials( + HINTERNET request, + winhttp_server *server, + const char *url_str, + git_credential_acquire_cb cred_cb, + void *cred_cb_payload) +{ + int allowed_types; + int error = 1; + + if (parse_unauthorized_response(&allowed_types, &server->auth_mechanisms, request) < 0) + return -1; + + if (allowed_types) { + git_credential_free(server->cred); + server->cred = NULL; + + /* Start with URL-specified credentials, if there were any. */ + if (!server->url_cred_presented && server->url.username && server->url.password) { + error = acquire_url_cred(&server->cred, allowed_types, server->url.username, server->url.password); + server->url_cred_presented = 1; + + if (error < 0) + return error; + } + + /* Next use the user-defined callback, if there is one. */ + if (error > 0 && cred_cb) { + error = cred_cb(&server->cred, url_str, server->url.username, allowed_types, cred_cb_payload); + + /* Treat GIT_PASSTHROUGH as though git_credential_acquire_cb isn't set */ + if (error == GIT_PASSTHROUGH) + error = 1; + else if (error < 0) + return error; + } + + /* Finally, invoke the fallback default credential lookup. */ + if (error > 0) { + error = acquire_fallback_cred(&server->cred, url_str, allowed_types); + + if (error < 0) + return error; + } + } + + /* + * No error occurred but we could not find appropriate credentials. + * This behaves like a pass-through. + */ + return error; +} + +static int winhttp_stream_read( + git_smart_subtransport_stream *stream, + char *buffer, + size_t buf_size, + size_t *bytes_read) +{ + winhttp_stream *s = (winhttp_stream *)stream; + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + DWORD dw_bytes_read; + char replay_count = 0; + int error; + +replay: + /* Enforce a reasonable cap on the number of replays */ + if (replay_count++ >= GIT_HTTP_REPLAY_MAX) { + git_error_set(GIT_ERROR_HTTP, "too many redirects or authentication replays"); + return GIT_ERROR; /* not GIT_EAUTH because the exact cause is not clear */ + } + + /* Connect if necessary */ + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + if (!s->received_response) { + DWORD status_code, status_code_length, content_type_length, bytes_written; + char expected_content_type_8[MAX_CONTENT_TYPE_LEN]; + wchar_t expected_content_type[MAX_CONTENT_TYPE_LEN], content_type[MAX_CONTENT_TYPE_LEN]; + + if (!s->sent_request) { + + if ((error = send_request(s, s->post_body_len, false)) < 0) + return error; + + s->sent_request = 1; + } + + if (s->chunked) { + GIT_ASSERT(s->verb == post_verb); + + /* Flush, if necessary */ + if (s->chunk_buffer_len > 0 && + write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + + /* Write the final chunk. */ + if (!WinHttpWriteData(s->request, + "0\r\n\r\n", 5, + &bytes_written)) { + git_error_set(GIT_ERROR_OS, "failed to write final chunk"); + return -1; + } + } + else if (s->post_body) { + char *buffer; + DWORD len = s->post_body_len, bytes_read; + + if (INVALID_SET_FILE_POINTER == SetFilePointer(s->post_body, + 0, 0, FILE_BEGIN) && + NO_ERROR != GetLastError()) { + git_error_set(GIT_ERROR_OS, "failed to reset file pointer"); + return -1; + } + + buffer = git__malloc(CACHED_POST_BODY_BUF_SIZE); + GIT_ERROR_CHECK_ALLOC(buffer); + + while (len > 0) { + DWORD bytes_written; + + if (!ReadFile(s->post_body, buffer, + min(CACHED_POST_BODY_BUF_SIZE, len), + &bytes_read, NULL) || + !bytes_read) { + git__free(buffer); + git_error_set(GIT_ERROR_OS, "failed to read from temp file"); + return -1; + } + + if (!WinHttpWriteData(s->request, buffer, + bytes_read, &bytes_written)) { + git__free(buffer); + git_error_set(GIT_ERROR_OS, "failed to write data"); + return -1; + } + + len -= bytes_read; + GIT_ASSERT(bytes_read == bytes_written); + } + + git__free(buffer); + + /* Eagerly close the temp file */ + CloseHandle(s->post_body); + s->post_body = NULL; + } + + if (!WinHttpReceiveResponse(s->request, 0)) { + git_error_set(GIT_ERROR_OS, "failed to receive response"); + return -1; + } + + /* Verify that we got a 200 back */ + status_code_length = sizeof(status_code); + + if (!WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &status_code, &status_code_length, + WINHTTP_NO_HEADER_INDEX)) { + git_error_set(GIT_ERROR_OS, "failed to retrieve status code"); + return -1; + } + + /* The implementation of WinHTTP prior to Windows 7 will not + * redirect to an identical URI. Some Git hosters use self-redirects + * as part of their DoS mitigation strategy. Check first to see if we + * have a redirect status code, and that we haven't already streamed + * a post body. (We can't replay a streamed POST.) */ + if (!s->chunked && + (HTTP_STATUS_MOVED == status_code || + HTTP_STATUS_REDIRECT == status_code || + (HTTP_STATUS_REDIRECT_METHOD == status_code && + get_verb == s->verb) || + HTTP_STATUS_REDIRECT_KEEP_VERB == status_code || + HTTP_STATUS_PERMANENT_REDIRECT == status_code)) { + + /* Check for Windows 7. This workaround is only necessary on + * Windows Vista and earlier. Windows 7 is version 6.1. */ + wchar_t *location; + DWORD location_length; + char *location8; + + /* OK, fetch the Location header from the redirect. */ + if (WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_LOCATION, + WINHTTP_HEADER_NAME_BY_INDEX, + WINHTTP_NO_OUTPUT_BUFFER, + &location_length, + WINHTTP_NO_HEADER_INDEX) || + GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + git_error_set(GIT_ERROR_OS, "failed to read Location header"); + return -1; + } + + location = git__malloc(location_length); + GIT_ERROR_CHECK_ALLOC(location); + + if (!WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_LOCATION, + WINHTTP_HEADER_NAME_BY_INDEX, + location, + &location_length, + WINHTTP_NO_HEADER_INDEX)) { + git_error_set(GIT_ERROR_OS, "failed to read Location header"); + git__free(location); + return -1; + } + + /* Convert the Location header to UTF-8 */ + if (git_utf8_from_16_alloc(&location8, location) < 0) { + git_error_set(GIT_ERROR_OS, "failed to convert Location header to UTF-8"); + git__free(location); + return -1; + } + + git__free(location); + + /* Replay the request */ + winhttp_stream_close(s); + + if (!git__prefixcmp_icase(location8, prefix_https)) { + bool follow = (t->owner->connect_opts.follow_redirects != GIT_REMOTE_REDIRECT_NONE); + + /* Upgrade to secure connection; disconnect and start over */ + if (git_net_url_apply_redirect(&t->server.url, location8, follow, s->service_url) < 0) { + git__free(location8); + return -1; + } + + winhttp_close_connection(t); + + if (winhttp_connect(t) < 0) + return -1; + } + + git__free(location8); + goto replay; + } + + /* Handle authentication failures */ + if (status_code == HTTP_STATUS_DENIED) { + int error = acquire_credentials(s->request, + &t->server, + t->owner->url, + t->owner->connect_opts.callbacks.credentials, + t->owner->connect_opts.callbacks.payload); + + if (error < 0) { + return error; + } else if (!error) { + GIT_ASSERT(t->server.cred); + winhttp_stream_close(s); + goto replay; + } + } else if (status_code == HTTP_STATUS_PROXY_AUTH_REQ) { + int error = acquire_credentials(s->request, + &t->proxy, + t->owner->connect_opts.proxy_opts.url, + t->owner->connect_opts.proxy_opts.credentials, + t->owner->connect_opts.proxy_opts.payload); + + if (error < 0) { + return error; + } else if (!error) { + GIT_ASSERT(t->proxy.cred); + winhttp_stream_close(s); + goto replay; + } + } + + if (HTTP_STATUS_OK != status_code) { + git_error_set(GIT_ERROR_HTTP, "request failed with status code: %lu", status_code); + return -1; + } + + /* Verify that we got the correct content-type back */ + if (post_verb == s->verb) + p_snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-result", s->service); + else + p_snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-advertisement", s->service); + + if (git_utf8_to_16(expected_content_type, MAX_CONTENT_TYPE_LEN, expected_content_type_8) < 0) { + git_error_set(GIT_ERROR_OS, "failed to convert expected content-type to wide characters"); + return -1; + } + + content_type_length = sizeof(content_type); + + if (!WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_CONTENT_TYPE, + WINHTTP_HEADER_NAME_BY_INDEX, + &content_type, &content_type_length, + WINHTTP_NO_HEADER_INDEX)) { + git_error_set(GIT_ERROR_OS, "failed to retrieve response content-type"); + return -1; + } + + if (wcscmp(expected_content_type, content_type)) { + git_error_set(GIT_ERROR_HTTP, "received unexpected content-type"); + return -1; + } + + s->received_response = 1; + } + + if (!WinHttpReadData(s->request, + (LPVOID)buffer, + (DWORD)buf_size, + &dw_bytes_read)) + { + git_error_set(GIT_ERROR_OS, "failed to read data"); + return -1; + } + + *bytes_read = dw_bytes_read; + + return 0; +} + +static int winhttp_stream_write_single( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + winhttp_stream *s = (winhttp_stream *)stream; + DWORD bytes_written; + int error; + + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + /* This implementation of write permits only a single call. */ + if (s->sent_request) { + git_error_set(GIT_ERROR_HTTP, "subtransport configured for only one write"); + return -1; + } + + if ((error = send_request(s, len, false)) < 0) + return error; + + s->sent_request = 1; + + if (!WinHttpWriteData(s->request, + (LPCVOID)buffer, + (DWORD)len, + &bytes_written)) { + git_error_set(GIT_ERROR_OS, "failed to write data"); + return -1; + } + + GIT_ASSERT((DWORD)len == bytes_written); + + return 0; +} + +static int put_uuid_string(LPWSTR buffer, size_t buffer_len_cch) +{ + UUID uuid; + RPC_STATUS status = UuidCreate(&uuid); + int result; + + if (RPC_S_OK != status && + RPC_S_UUID_LOCAL_ONLY != status && + RPC_S_UUID_NO_ADDRESS != status) { + git_error_set(GIT_ERROR_HTTP, "unable to generate name for temp file"); + return -1; + } + + if (buffer_len_cch < UUID_LENGTH_CCH + 1) { + git_error_set(GIT_ERROR_HTTP, "buffer too small for name of temp file"); + return -1; + } + +#if !defined(__MINGW32__) || defined(MINGW_HAS_SECURE_API) + result = swprintf_s(buffer, buffer_len_cch, +#else + result = wsprintfW(buffer, +#endif + L"%08x%04x%04x%02x%02x%02x%02x%02x%02x%02x%02x", + uuid.Data1, uuid.Data2, uuid.Data3, + uuid.Data4[0], uuid.Data4[1], uuid.Data4[2], uuid.Data4[3], + uuid.Data4[4], uuid.Data4[5], uuid.Data4[6], uuid.Data4[7]); + + if (result < UUID_LENGTH_CCH) { + git_error_set(GIT_ERROR_OS, "unable to generate name for temp file"); + return -1; + } + + return 0; +} + +static int get_temp_file(LPWSTR buffer, DWORD buffer_len_cch) +{ + size_t len; + + if (!GetTempPathW(buffer_len_cch, buffer)) { + git_error_set(GIT_ERROR_OS, "failed to get temp path"); + return -1; + } + + len = wcslen(buffer); + + if (buffer[len - 1] != '\\' && len < buffer_len_cch) + buffer[len++] = '\\'; + + if (put_uuid_string(&buffer[len], (size_t)buffer_len_cch - len) < 0) + return -1; + + return 0; +} + +static int winhttp_stream_write_buffered( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + winhttp_stream *s = (winhttp_stream *)stream; + DWORD bytes_written; + + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + /* Buffer the payload, using a temporary file so we delegate + * memory management of the data to the operating system. */ + if (!s->post_body) { + wchar_t temp_path[MAX_PATH + 1]; + + if (get_temp_file(temp_path, MAX_PATH + 1) < 0) + return -1; + + s->post_body = CreateFileW(temp_path, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_DELETE, NULL, + CREATE_NEW, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE | FILE_FLAG_SEQUENTIAL_SCAN, + NULL); + + if (INVALID_HANDLE_VALUE == s->post_body) { + s->post_body = NULL; + git_error_set(GIT_ERROR_OS, "failed to create temporary file"); + return -1; + } + } + + if (!WriteFile(s->post_body, buffer, (DWORD)len, &bytes_written, NULL)) { + git_error_set(GIT_ERROR_OS, "failed to write to temporary file"); + return -1; + } + + GIT_ASSERT((DWORD)len == bytes_written); + + s->post_body_len += bytes_written; + + return 0; +} + +static int winhttp_stream_write_chunked( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + winhttp_stream *s = (winhttp_stream *)stream; + int error; + + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + if (!s->sent_request) { + /* Send Transfer-Encoding: chunked header */ + if (!WinHttpAddRequestHeaders(s->request, + transfer_encoding, (ULONG) -1L, + WINHTTP_ADDREQ_FLAG_ADD)) { + git_error_set(GIT_ERROR_OS, "failed to add a header to the request"); + return -1; + } + + if ((error = send_request(s, 0, true)) < 0) + return error; + + s->sent_request = 1; + } + + if (len > CACHED_POST_BODY_BUF_SIZE) { + /* Flush, if necessary */ + if (s->chunk_buffer_len > 0) { + if (write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + } + + /* Write chunk directly */ + if (write_chunk(s->request, buffer, len) < 0) + return -1; + } + else { + /* Append as much to the buffer as we can */ + int count = (int)min(CACHED_POST_BODY_BUF_SIZE - s->chunk_buffer_len, len); + + if (!s->chunk_buffer) { + s->chunk_buffer = git__malloc(CACHED_POST_BODY_BUF_SIZE); + GIT_ERROR_CHECK_ALLOC(s->chunk_buffer); + } + + memcpy(s->chunk_buffer + s->chunk_buffer_len, buffer, count); + s->chunk_buffer_len += count; + buffer += count; + len -= count; + + /* Is the buffer full? If so, then flush */ + if (CACHED_POST_BODY_BUF_SIZE == s->chunk_buffer_len) { + if (write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + + /* Is there any remaining data from the source? */ + if (len > 0) { + memcpy(s->chunk_buffer, buffer, len); + s->chunk_buffer_len = (unsigned int)len; + } + } + } + + return 0; +} + +static void winhttp_stream_free(git_smart_subtransport_stream *stream) +{ + winhttp_stream *s = (winhttp_stream *)stream; + + winhttp_stream_close(s); + git__free(s); +} + +static int winhttp_stream_alloc(winhttp_subtransport *t, winhttp_stream **stream) +{ + winhttp_stream *s; + + if (!stream) + return -1; + + s = git__calloc(1, sizeof(winhttp_stream)); + GIT_ERROR_CHECK_ALLOC(s); + + s->parent.subtransport = &t->parent; + s->parent.read = winhttp_stream_read; + s->parent.write = winhttp_stream_write_single; + s->parent.free = winhttp_stream_free; + + *stream = s; + + return 0; +} + +static int winhttp_uploadpack_ls( + winhttp_subtransport *t, + winhttp_stream *s) +{ + GIT_UNUSED(t); + + s->service = upload_pack_service; + s->service_url = upload_pack_ls_service_url; + s->verb = get_verb; + + return 0; +} + +static int winhttp_uploadpack( + winhttp_subtransport *t, + winhttp_stream *s) +{ + GIT_UNUSED(t); + + s->service = upload_pack_service; + s->service_url = upload_pack_service_url; + s->verb = post_verb; + + return 0; +} + +static int winhttp_receivepack_ls( + winhttp_subtransport *t, + winhttp_stream *s) +{ + GIT_UNUSED(t); + + s->service = receive_pack_service; + s->service_url = receive_pack_ls_service_url; + s->verb = get_verb; + + return 0; +} + +static int winhttp_receivepack( + winhttp_subtransport *t, + winhttp_stream *s) +{ + GIT_UNUSED(t); + + /* WinHTTP only supports Transfer-Encoding: chunked + * on Windows Vista (NT 6.0) and higher. */ + s->chunked = git_has_win32_version(6, 0, 0); + + if (s->chunked) + s->parent.write = winhttp_stream_write_chunked; + else + s->parent.write = winhttp_stream_write_buffered; + + s->service = receive_pack_service; + s->service_url = receive_pack_service_url; + s->verb = post_verb; + + return 0; +} + +static int winhttp_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) +{ + winhttp_subtransport *t = (winhttp_subtransport *)subtransport; + winhttp_stream *s; + int ret = -1; + + if (!t->connection) + if ((ret = git_net_url_parse(&t->server.url, url)) < 0 || + (ret = winhttp_connect(t)) < 0) + return ret; + + if (winhttp_stream_alloc(t, &s) < 0) + return -1; + + if (!stream) + return -1; + + switch (action) + { + case GIT_SERVICE_UPLOADPACK_LS: + ret = winhttp_uploadpack_ls(t, s); + break; + + case GIT_SERVICE_UPLOADPACK: + ret = winhttp_uploadpack(t, s); + break; + + case GIT_SERVICE_RECEIVEPACK_LS: + ret = winhttp_receivepack_ls(t, s); + break; + + case GIT_SERVICE_RECEIVEPACK: + ret = winhttp_receivepack(t, s); + break; + + default: + GIT_ASSERT(0); + } + + if (!ret) + *stream = &s->parent; + + return ret; +} + +static int winhttp_close(git_smart_subtransport *subtransport) +{ + winhttp_subtransport *t = (winhttp_subtransport *)subtransport; + + git_net_url_dispose(&t->server.url); + git_net_url_dispose(&t->proxy.url); + + if (t->server.cred) { + t->server.cred->free(t->server.cred); + t->server.cred = NULL; + } + + if (t->proxy.cred) { + t->proxy.cred->free(t->proxy.cred); + t->proxy.cred = NULL; + } + + return winhttp_close_connection(t); +} + +static void winhttp_free(git_smart_subtransport *subtransport) +{ + winhttp_subtransport *t = (winhttp_subtransport *)subtransport; + + winhttp_close(subtransport); + + git__free(t); +} + +int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner, void *param) +{ + winhttp_subtransport *t; + + GIT_UNUSED(param); + + if (!out) + return -1; + + t = git__calloc(1, sizeof(winhttp_subtransport)); + GIT_ERROR_CHECK_ALLOC(t); + + t->owner = (transport_smart *)owner; + t->parent.action = winhttp_action; + t->parent.close = winhttp_close; + t->parent.free = winhttp_free; + + *out = (git_smart_subtransport *) t; + return 0; +} + +#endif /* GIT_WINHTTP */ diff --git a/src/libgit2/tree-cache.c b/src/libgit2/tree-cache.c new file mode 100644 index 0000000..95d8798 --- /dev/null +++ b/src/libgit2/tree-cache.c @@ -0,0 +1,287 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "tree-cache.h" + +#include "pool.h" +#include "tree.h" + +static git_tree_cache *find_child( + const git_tree_cache *tree, const char *path, const char *end) +{ + size_t i, dirlen = end ? (size_t)(end - path) : strlen(path); + + for (i = 0; i < tree->children_count; ++i) { + git_tree_cache *child = tree->children[i]; + + if (child->namelen == dirlen && !memcmp(path, child->name, dirlen)) + return child; + } + + return NULL; +} + +void git_tree_cache_invalidate_path(git_tree_cache *tree, const char *path) +{ + const char *ptr = path, *end; + + if (tree == NULL) + return; + + tree->entry_count = -1; + + while (ptr != NULL) { + end = strchr(ptr, '/'); + + if (end == NULL) /* End of path */ + break; + + tree = find_child(tree, ptr, end); + if (tree == NULL) /* We don't have that tree */ + return; + + tree->entry_count = -1; + ptr = end + 1; + } +} + +const git_tree_cache *git_tree_cache_get(const git_tree_cache *tree, const char *path) +{ + const char *ptr = path, *end; + + if (tree == NULL) { + return NULL; + } + + while (1) { + end = strchr(ptr, '/'); + + tree = find_child(tree, ptr, end); + if (tree == NULL) /* Can't find it */ + return NULL; + + if (end == NULL || *end + 1 == '\0') + return tree; + + ptr = end + 1; + } +} + +static int read_tree_internal( + git_tree_cache **out, + const char **buffer_in, + const char *buffer_end, + git_oid_t oid_type, + git_pool *pool) +{ + git_tree_cache *tree = NULL; + const char *name_start, *buffer; + size_t oid_size = git_oid_size(oid_type); + int count; + + buffer = name_start = *buffer_in; + + if ((buffer = memchr(buffer, '\0', buffer_end - buffer)) == NULL) + goto corrupted; + + if (++buffer >= buffer_end) + goto corrupted; + + if (git_tree_cache_new(&tree, name_start, oid_type, pool) < 0) + return -1; + + /* Blank-terminated ASCII decimal number of entries in this tree */ + if (git__strntol32(&count, buffer, buffer_end - buffer, &buffer, 10) < 0) + goto corrupted; + + tree->entry_count = count; + + if (*buffer != ' ' || ++buffer >= buffer_end) + goto corrupted; + + /* Number of children of the tree, newline-terminated */ + if (git__strntol32(&count, buffer, buffer_end - buffer, &buffer, 10) < 0 || count < 0) + goto corrupted; + + tree->children_count = count; + + if (*buffer != '\n' || ++buffer > buffer_end) + goto corrupted; + + /* The OID is only there if it's not invalidated */ + if (tree->entry_count >= 0) { + /* 160-bit SHA-1 for this tree and it's children */ + if (buffer + oid_size > buffer_end) + goto corrupted; + + git_oid__fromraw(&tree->oid, (const unsigned char *)buffer, oid_type); + buffer += oid_size; + } + + /* Parse children: */ + if (tree->children_count > 0) { + size_t i, bufsize; + + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&bufsize, tree->children_count, sizeof(git_tree_cache*)); + + tree->children = git_pool_malloc(pool, bufsize); + GIT_ERROR_CHECK_ALLOC(tree->children); + + memset(tree->children, 0x0, bufsize); + + for (i = 0; i < tree->children_count; ++i) { + if (read_tree_internal(&tree->children[i], &buffer, buffer_end, oid_type, pool) < 0) + goto corrupted; + } + } + + *buffer_in = buffer; + *out = tree; + return 0; + + corrupted: + git_error_set(GIT_ERROR_INDEX, "corrupted TREE extension in index"); + return -1; +} + +int git_tree_cache_read( + git_tree_cache **tree, + const char *buffer, + size_t buffer_size, + git_oid_t oid_type, + git_pool *pool) +{ + const char *buffer_end = buffer + buffer_size; + + if (read_tree_internal(tree, &buffer, buffer_end, oid_type, pool) < 0) + return -1; + + if (buffer < buffer_end) { + git_error_set(GIT_ERROR_INDEX, "corrupted TREE extension in index (unexpected trailing data)"); + return -1; + } + + return 0; +} + +static int read_tree_recursive(git_tree_cache *cache, const git_tree *tree, git_pool *pool) +{ + git_repository *repo; + size_t i, j, nentries, ntrees, alloc_size; + int error; + + repo = git_tree_owner(tree); + + git_oid_cpy(&cache->oid, git_tree_id(tree)); + nentries = git_tree_entrycount(tree); + + /* + * We make sure we know how many trees we need to allocate for + * so we don't have to realloc and change the pointers for the + * parents. + */ + ntrees = 0; + for (i = 0; i < nentries; i++) { + const git_tree_entry *entry; + + entry = git_tree_entry_byindex(tree, i); + if (git_tree_entry_filemode(entry) == GIT_FILEMODE_TREE) + ntrees++; + } + + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&alloc_size, ntrees, sizeof(git_tree_cache *)); + + cache->children_count = ntrees; + cache->children = git_pool_mallocz(pool, alloc_size); + GIT_ERROR_CHECK_ALLOC(cache->children); + + j = 0; + for (i = 0; i < nentries; i++) { + const git_tree_entry *entry; + git_tree *subtree; + + entry = git_tree_entry_byindex(tree, i); + if (git_tree_entry_filemode(entry) != GIT_FILEMODE_TREE) { + cache->entry_count++; + continue; + } + + if ((error = git_tree_cache_new(&cache->children[j], git_tree_entry_name(entry), cache->oid_type, pool)) < 0) + return error; + + if ((error = git_tree_lookup(&subtree, repo, git_tree_entry_id(entry))) < 0) + return error; + + error = read_tree_recursive(cache->children[j], subtree, pool); + git_tree_free(subtree); + cache->entry_count += cache->children[j]->entry_count; + j++; + + if (error < 0) + return error; + } + + return 0; +} + +int git_tree_cache_read_tree(git_tree_cache **out, const git_tree *tree, git_oid_t oid_type, git_pool *pool) +{ + int error; + git_tree_cache *cache; + + if ((error = git_tree_cache_new(&cache, "", oid_type, pool)) < 0) + return error; + + if ((error = read_tree_recursive(cache, tree, pool)) < 0) + return error; + + *out = cache; + return 0; +} + +int git_tree_cache_new(git_tree_cache **out, const char *name, git_oid_t oid_type, git_pool *pool) +{ + size_t name_len, alloc_size; + git_tree_cache *tree; + + name_len = strlen(name); + + GIT_ERROR_CHECK_ALLOC_ADD3(&alloc_size, sizeof(git_tree_cache), name_len, 1); + + tree = git_pool_malloc(pool, alloc_size); + GIT_ERROR_CHECK_ALLOC(tree); + + memset(tree, 0x0, sizeof(git_tree_cache)); + /* NUL-terminated tree name */ + tree->oid_type = oid_type; + tree->namelen = name_len; + memcpy(tree->name, name, name_len); + tree->name[name_len] = '\0'; + + *out = tree; + return 0; +} + +static void write_tree(git_str *out, git_tree_cache *tree) +{ + size_t i; + + git_str_printf(out, "%s%c%"PRIdZ" %"PRIuZ"\n", tree->name, 0, tree->entry_count, tree->children_count); + + if (tree->entry_count != -1) + git_str_put(out, (char *)&tree->oid.id, git_oid_size(tree->oid_type)); + + for (i = 0; i < tree->children_count; i++) + write_tree(out, tree->children[i]); +} + +int git_tree_cache_write(git_str *out, git_tree_cache *tree) +{ + write_tree(out, tree); + + return git_str_oom(out) ? -1 : 0; +} diff --git a/src/libgit2/tree-cache.h b/src/libgit2/tree-cache.h new file mode 100644 index 0000000..e4a73f2 --- /dev/null +++ b/src/libgit2/tree-cache.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_tree_cache_h__ +#define INCLUDE_tree_cache_h__ + +#include "common.h" + +#include "pool.h" +#include "str.h" +#include "git2/oid.h" + +typedef struct git_tree_cache { + struct git_tree_cache **children; + size_t children_count; + + git_oid_t oid_type; + + ssize_t entry_count; + git_oid oid; + size_t namelen; + char name[GIT_FLEX_ARRAY]; +} git_tree_cache; + +int git_tree_cache_write(git_str *out, git_tree_cache *tree); +int git_tree_cache_read(git_tree_cache **tree, const char *buffer, size_t buffer_size, git_oid_t oid_type, git_pool *pool); +void git_tree_cache_invalidate_path(git_tree_cache *tree, const char *path); +const git_tree_cache *git_tree_cache_get(const git_tree_cache *tree, const char *path); +int git_tree_cache_new(git_tree_cache **out, const char *name, git_oid_t oid_type, git_pool *pool); +/** + * Read a tree as the root of the tree cache (like for `git read-tree`) + */ +int git_tree_cache_read_tree(git_tree_cache **out, const git_tree *tree, git_oid_t oid_type, git_pool *pool); +void git_tree_cache_free(git_tree_cache *tree); + +#endif diff --git a/src/libgit2/tree.c b/src/libgit2/tree.c new file mode 100644 index 0000000..236a87f --- /dev/null +++ b/src/libgit2/tree.c @@ -0,0 +1,1330 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "tree.h" + +#include "commit.h" +#include "git2/repository.h" +#include "git2/object.h" +#include "futils.h" +#include "tree-cache.h" +#include "index.h" +#include "path.h" + +#define DEFAULT_TREE_SIZE 16 +#define MAX_FILEMODE_BYTES 6 + +#define TREE_ENTRY_CHECK_NAMELEN(n) \ + if (n > UINT16_MAX) { git_error_set(GIT_ERROR_INVALID, "tree entry path too long"); } + +static bool valid_filemode(const int filemode) +{ + return (filemode == GIT_FILEMODE_TREE + || filemode == GIT_FILEMODE_BLOB + || filemode == GIT_FILEMODE_BLOB_EXECUTABLE + || filemode == GIT_FILEMODE_LINK + || filemode == GIT_FILEMODE_COMMIT); +} + +GIT_INLINE(git_filemode_t) normalize_filemode(git_filemode_t filemode) +{ + /* Tree bits set, but it's not a commit */ + if (GIT_MODE_TYPE(filemode) == GIT_FILEMODE_TREE) + return GIT_FILEMODE_TREE; + + /* If any of the x bits are set */ + if (GIT_PERMS_IS_EXEC(filemode)) + return GIT_FILEMODE_BLOB_EXECUTABLE; + + /* 16XXXX means commit */ + if (GIT_MODE_TYPE(filemode) == GIT_FILEMODE_COMMIT) + return GIT_FILEMODE_COMMIT; + + /* 12XXXX means symlink */ + if (GIT_MODE_TYPE(filemode) == GIT_FILEMODE_LINK) + return GIT_FILEMODE_LINK; + + /* Otherwise, return a blob */ + return GIT_FILEMODE_BLOB; +} + +static int valid_entry_name(git_repository *repo, const char *filename) +{ + return *filename != '\0' && + git_path_is_valid(repo, filename, 0, + GIT_FS_PATH_REJECT_TRAVERSAL | GIT_PATH_REJECT_DOT_GIT | GIT_FS_PATH_REJECT_SLASH); +} + +static int entry_sort_cmp(const void *a, const void *b) +{ + const git_tree_entry *e1 = (const git_tree_entry *)a; + const git_tree_entry *e2 = (const git_tree_entry *)b; + + return git_fs_path_cmp( + e1->filename, e1->filename_len, git_tree_entry__is_tree(e1), + e2->filename, e2->filename_len, git_tree_entry__is_tree(e2), + git__strncmp); +} + +int git_tree_entry_cmp(const git_tree_entry *e1, const git_tree_entry *e2) +{ + return entry_sort_cmp(e1, e2); +} + +/** + * Allocate a new self-contained entry, with enough space after it to + * store the filename and the id. + */ +static git_tree_entry *alloc_entry(const char *filename, size_t filename_len, const git_oid *id) +{ + git_tree_entry *entry = NULL; + char *filename_ptr; + size_t tree_len; + +#ifdef GIT_EXPERIMENTAL_SHA256 + size_t oid_size = git_oid_size(id->type); +#else + size_t oid_size = GIT_OID_SHA1_SIZE; +#endif + + TREE_ENTRY_CHECK_NAMELEN(filename_len); + + if (GIT_ADD_SIZET_OVERFLOW(&tree_len, sizeof(git_tree_entry), filename_len) || + GIT_ADD_SIZET_OVERFLOW(&tree_len, tree_len, 1) || + GIT_ADD_SIZET_OVERFLOW(&tree_len, tree_len, oid_size)) + return NULL; + + entry = git__calloc(1, tree_len); + if (!entry) + return NULL; + + filename_ptr = ((char *) entry) + sizeof(git_tree_entry); + memcpy(filename_ptr, filename, filename_len); + entry->filename = filename_ptr; + entry->filename_len = (uint16_t)filename_len; + + git_oid_cpy(&entry->oid, id); + + return entry; +} + +struct tree_key_search { + const char *filename; + uint16_t filename_len; +}; + +static int homing_search_cmp(const void *key, const void *array_member) +{ + const struct tree_key_search *ksearch = key; + const git_tree_entry *entry = array_member; + + const uint16_t len1 = ksearch->filename_len; + const uint16_t len2 = entry->filename_len; + + return memcmp( + ksearch->filename, + entry->filename, + len1 < len2 ? len1 : len2 + ); +} + +/* + * Search for an entry in a given tree. + * + * Note that this search is performed in two steps because + * of the way tree entries are sorted internally in git: + * + * Entries in a tree are not sorted alphabetically; two entries + * with the same root prefix will have different positions + * depending on whether they are folders (subtrees) or normal files. + * + * Consequently, it is not possible to find an entry on the tree + * with a binary search if you don't know whether the filename + * you're looking for is a folder or a normal file. + * + * To work around this, we first perform a homing binary search + * on the tree, using the minimal length root prefix of our filename. + * Once the comparisons for this homing search start becoming + * ambiguous because of folder vs file sorting, we look linearly + * around the area for our target file. + */ +static int tree_key_search( + size_t *at_pos, + const git_tree *tree, + const char *filename, + size_t filename_len) +{ + struct tree_key_search ksearch; + const git_tree_entry *entry; + size_t homing, i; + + TREE_ENTRY_CHECK_NAMELEN(filename_len); + + ksearch.filename = filename; + ksearch.filename_len = (uint16_t)filename_len; + + /* Initial homing search; find an entry on the tree with + * the same prefix as the filename we're looking for */ + + if (git_array_search(&homing, + tree->entries, &homing_search_cmp, &ksearch) < 0) + return GIT_ENOTFOUND; /* just a signal error; not passed back to user */ + + /* We found a common prefix. Look forward as long as + * there are entries that share the common prefix */ + for (i = homing; i < tree->entries.size; ++i) { + entry = git_array_get(tree->entries, i); + + if (homing_search_cmp(&ksearch, entry) < 0) + break; + + if (entry->filename_len == filename_len && + memcmp(filename, entry->filename, filename_len) == 0) { + if (at_pos) + *at_pos = i; + + return 0; + } + } + + /* If we haven't found our filename yet, look backwards + * too as long as we have entries with the same prefix */ + if (homing > 0) { + i = homing - 1; + + do { + entry = git_array_get(tree->entries, i); + + if (homing_search_cmp(&ksearch, entry) > 0) + break; + + if (entry->filename_len == filename_len && + memcmp(filename, entry->filename, filename_len) == 0) { + if (at_pos) + *at_pos = i; + + return 0; + } + } while (i-- > 0); + } + + /* The filename doesn't exist at all */ + return GIT_ENOTFOUND; +} + +void git_tree_entry_free(git_tree_entry *entry) +{ + if (entry == NULL) + return; + + git__free(entry); +} + +int git_tree_entry_dup(git_tree_entry **dest, const git_tree_entry *source) +{ + git_tree_entry *cpy; + + GIT_ASSERT_ARG(source); + + cpy = alloc_entry(source->filename, source->filename_len, &source->oid); + if (cpy == NULL) + return -1; + + cpy->attr = source->attr; + + *dest = cpy; + return 0; +} + +void git_tree__free(void *_tree) +{ + git_tree *tree = _tree; + + git_odb_object_free(tree->odb_obj); + git_array_clear(tree->entries); + git__free(tree); +} + +git_filemode_t git_tree_entry_filemode(const git_tree_entry *entry) +{ + return normalize_filemode(entry->attr); +} + +git_filemode_t git_tree_entry_filemode_raw(const git_tree_entry *entry) +{ + return entry->attr; +} + +const char *git_tree_entry_name(const git_tree_entry *entry) +{ + GIT_ASSERT_ARG_WITH_RETVAL(entry, NULL); + return entry->filename; +} + +const git_oid *git_tree_entry_id(const git_tree_entry *entry) +{ + GIT_ASSERT_ARG_WITH_RETVAL(entry, NULL); + return &entry->oid; +} + +git_object_t git_tree_entry_type(const git_tree_entry *entry) +{ + GIT_ASSERT_ARG_WITH_RETVAL(entry, GIT_OBJECT_INVALID); + + if (S_ISGITLINK(entry->attr)) + return GIT_OBJECT_COMMIT; + else if (S_ISDIR(entry->attr)) + return GIT_OBJECT_TREE; + else + return GIT_OBJECT_BLOB; +} + +int git_tree_entry_to_object( + git_object **object_out, + git_repository *repo, + const git_tree_entry *entry) +{ + GIT_ASSERT_ARG(entry); + GIT_ASSERT_ARG(object_out); + + return git_object_lookup(object_out, repo, &entry->oid, GIT_OBJECT_ANY); +} + +static const git_tree_entry *entry_fromname( + const git_tree *tree, const char *name, size_t name_len) +{ + size_t idx; + + if (tree_key_search(&idx, tree, name, name_len) < 0) + return NULL; + + return git_array_get(tree->entries, idx); +} + +const git_tree_entry *git_tree_entry_byname( + const git_tree *tree, const char *filename) +{ + GIT_ASSERT_ARG_WITH_RETVAL(tree, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(filename, NULL); + + return entry_fromname(tree, filename, strlen(filename)); +} + +const git_tree_entry *git_tree_entry_byindex( + const git_tree *tree, size_t idx) +{ + GIT_ASSERT_ARG_WITH_RETVAL(tree, NULL); + return git_array_get(tree->entries, idx); +} + +const git_tree_entry *git_tree_entry_byid( + const git_tree *tree, const git_oid *id) +{ + size_t i; + const git_tree_entry *e; + + GIT_ASSERT_ARG_WITH_RETVAL(tree, NULL); + + git_array_foreach(tree->entries, i, e) { + if (git_oid_equal(&e->oid, id)) + return e; + } + + return NULL; +} + +size_t git_tree_entrycount(const git_tree *tree) +{ + GIT_ASSERT_ARG_WITH_RETVAL(tree, 0); + return tree->entries.size; +} + +size_t git_treebuilder_entrycount(git_treebuilder *bld) +{ + GIT_ASSERT_ARG_WITH_RETVAL(bld, 0); + + return git_strmap_size(bld->map); +} + +GIT_INLINE(void) set_error(const char *str, const char *path) +{ + if (path) + git_error_set(GIT_ERROR_TREE, "%s - %s", str, path); + else + git_error_set(GIT_ERROR_TREE, "%s", str); +} + +static int tree_error(const char *str, const char *path) +{ + set_error(str, path); + return -1; +} + +static int tree_parse_error(const char *str, const char *path) +{ + set_error(str, path); + return GIT_EINVALID; +} + +static int parse_mode(uint16_t *mode_out, const char *buffer, size_t buffer_len, const char **buffer_out) +{ + int32_t mode; + int error; + + if (!buffer_len || git__isspace(*buffer)) + return -1; + + if ((error = git__strntol32(&mode, buffer, buffer_len, buffer_out, 8)) < 0) + return error; + + if (mode < 0 || mode > UINT16_MAX) + return -1; + + *mode_out = mode; + + return 0; +} + +int git_tree__parse_raw(void *_tree, const char *data, size_t size, git_oid_t oid_type) +{ + git_tree *tree = _tree; + const char *buffer; + const char *buffer_end; + const long oid_size = (long)git_oid_size(oid_type); + + buffer = data; + buffer_end = buffer + size; + + tree->odb_obj = NULL; + git_array_init_to_size(tree->entries, DEFAULT_TREE_SIZE); + GIT_ERROR_CHECK_ARRAY(tree->entries); + + while (buffer < buffer_end) { + git_tree_entry *entry; + size_t filename_len; + const char *nul; + uint16_t attr; + + if (parse_mode(&attr, buffer, buffer_end - buffer, &buffer) < 0 || !buffer) + return tree_parse_error("failed to parse tree: can't parse filemode", NULL); + + if (buffer >= buffer_end || (*buffer++) != ' ') + return tree_parse_error("failed to parse tree: missing space after filemode", NULL); + + if ((nul = memchr(buffer, 0, buffer_end - buffer)) == NULL) + return tree_parse_error("failed to parse tree: object is corrupted", NULL); + + if ((filename_len = nul - buffer) == 0 || filename_len > UINT16_MAX) + return tree_parse_error("failed to parse tree: can't parse filename", NULL); + + if ((buffer_end - (nul + 1)) < (long)oid_size) + return tree_parse_error("failed to parse tree: can't parse OID", NULL); + + /* Allocate the entry */ + entry = git_array_alloc(tree->entries); + GIT_ERROR_CHECK_ALLOC(entry); + + entry->attr = attr; + entry->filename_len = (uint16_t)filename_len; + entry->filename = buffer; + buffer += filename_len + 1; + + git_oid__fromraw(&entry->oid, (unsigned char *)buffer, oid_type); + buffer += oid_size; + } + + return 0; +} + +int git_tree__parse(void *_tree, git_odb_object *odb_obj, git_oid_t oid_type) +{ + git_tree *tree = _tree; + const char *data = git_odb_object_data(odb_obj); + size_t size = git_odb_object_size(odb_obj); + int error; + + if ((error = git_tree__parse_raw(tree, data, size, oid_type)) < 0 || + (error = git_odb_object_dup(&tree->odb_obj, odb_obj)) < 0) + return error; + + return error; +} + +static size_t find_next_dir(const char *dirname, git_index *index, size_t start) +{ + size_t dirlen, i, entries = git_index_entrycount(index); + + dirlen = strlen(dirname); + for (i = start; i < entries; ++i) { + const git_index_entry *entry = git_index_get_byindex(index, i); + if (strlen(entry->path) < dirlen || + memcmp(entry->path, dirname, dirlen) || + (dirlen > 0 && entry->path[dirlen] != '/')) { + break; + } + } + + return i; +} + +static git_object_t otype_from_mode(git_filemode_t filemode) +{ + switch (filemode) { + case GIT_FILEMODE_TREE: + return GIT_OBJECT_TREE; + case GIT_FILEMODE_COMMIT: + return GIT_OBJECT_COMMIT; + default: + return GIT_OBJECT_BLOB; + } +} + +static int check_entry(git_repository *repo, const char *filename, const git_oid *id, git_filemode_t filemode) +{ + if (!valid_filemode(filemode)) + return tree_error("failed to insert entry: invalid filemode for file", filename); + + if (!valid_entry_name(repo, filename)) + return tree_error("failed to insert entry: invalid name for a tree entry", filename); + + if (git_oid_is_zero(id)) + return tree_error("failed to insert entry: invalid null OID", filename); + + if (filemode != GIT_FILEMODE_COMMIT && + !git_object__is_valid(repo, id, otype_from_mode(filemode))) + return tree_error("failed to insert entry: invalid object specified", filename); + + return 0; +} + +static int git_treebuilder__write_with_buffer( + git_oid *oid, + git_treebuilder *bld, + git_str *buf) +{ + int error = 0; + size_t i, entrycount; + git_odb *odb; + git_tree_entry *entry; + git_vector entries = GIT_VECTOR_INIT; + size_t oid_size = git_oid_size(bld->repo->oid_type); + + git_str_clear(buf); + + entrycount = git_strmap_size(bld->map); + if ((error = git_vector_init(&entries, entrycount, entry_sort_cmp)) < 0) + goto out; + + if (buf->asize == 0 && + (error = git_str_grow(buf, entrycount * 72)) < 0) + goto out; + + git_strmap_foreach_value(bld->map, entry, { + if ((error = git_vector_insert(&entries, entry)) < 0) + goto out; + }); + + git_vector_sort(&entries); + + for (i = 0; i < entries.length && !error; ++i) { + entry = git_vector_get(&entries, i); + + git_str_printf(buf, "%o ", entry->attr); + git_str_put(buf, entry->filename, entry->filename_len + 1); + git_str_put(buf, (char *)entry->oid.id, oid_size); + + if (git_str_oom(buf)) { + error = -1; + goto out; + } + } + + if ((error = git_repository_odb__weakptr(&odb, bld->repo)) == 0) + error = git_odb_write(oid, odb, buf->ptr, buf->size, GIT_OBJECT_TREE); + +out: + git_vector_free(&entries); + + return error; +} + +static int append_entry( + git_treebuilder *bld, + const char *filename, + const git_oid *id, + git_filemode_t filemode, + bool validate) +{ + git_tree_entry *entry; + int error = 0; + + if (validate && ((error = check_entry(bld->repo, filename, id, filemode)) < 0)) + return error; + + entry = alloc_entry(filename, strlen(filename), id); + GIT_ERROR_CHECK_ALLOC(entry); + + entry->attr = (uint16_t)filemode; + + if ((error = git_strmap_set(bld->map, entry->filename, entry)) < 0) { + git_tree_entry_free(entry); + git_error_set(GIT_ERROR_TREE, "failed to append entry %s to the tree builder", filename); + return -1; + } + + return 0; +} + +static int write_tree( + git_oid *oid, + git_repository *repo, + git_index *index, + const char *dirname, + size_t start, + git_str *shared_buf) +{ + git_treebuilder *bld = NULL; + size_t i, entries = git_index_entrycount(index); + int error; + size_t dirname_len = strlen(dirname); + const git_tree_cache *cache; + + cache = git_tree_cache_get(index->tree, dirname); + if (cache != NULL && cache->entry_count >= 0){ + git_oid_cpy(oid, &cache->oid); + return (int)find_next_dir(dirname, index, start); + } + + if ((error = git_treebuilder_new(&bld, repo, NULL)) < 0 || bld == NULL) + return -1; + + /* + * This loop is unfortunate, but necessary. The index doesn't have + * any directories, so we need to handle that manually, and we + * need to keep track of the current position. + */ + for (i = start; i < entries; ++i) { + const git_index_entry *entry = git_index_get_byindex(index, i); + const char *filename, *next_slash; + + /* + * If we've left our (sub)tree, exit the loop and return. The + * first check is an early out (and security for the + * third). The second check is a simple prefix comparison. The + * third check catches situations where there is a directory + * win32/sys and a file win32mmap.c. Without it, the following + * code believes there is a file win32/mmap.c + */ + if (strlen(entry->path) < dirname_len || + memcmp(entry->path, dirname, dirname_len) || + (dirname_len > 0 && entry->path[dirname_len] != '/')) { + break; + } + + filename = entry->path + dirname_len; + if (*filename == '/') + filename++; + next_slash = strchr(filename, '/'); + if (next_slash) { + git_oid sub_oid; + int written; + char *subdir, *last_comp; + + subdir = git__strndup(entry->path, next_slash - entry->path); + GIT_ERROR_CHECK_ALLOC(subdir); + + /* Write out the subtree */ + written = write_tree(&sub_oid, repo, index, subdir, i, shared_buf); + if (written < 0) { + git__free(subdir); + goto on_error; + } else { + i = written - 1; /* -1 because of the loop increment */ + } + + /* + * We need to figure out what we want toinsert + * into this tree. If we're traversing + * deps/zlib/, then we only want to write + * 'zlib' into the tree. + */ + last_comp = strrchr(subdir, '/'); + if (last_comp) { + last_comp++; /* Get rid of the '/' */ + } else { + last_comp = subdir; + } + + error = append_entry(bld, last_comp, &sub_oid, S_IFDIR, true); + git__free(subdir); + if (error < 0) + goto on_error; + } else { + error = append_entry(bld, filename, &entry->id, entry->mode, true); + if (error < 0) + goto on_error; + } + } + + if (git_treebuilder__write_with_buffer(oid, bld, shared_buf) < 0) + goto on_error; + + git_treebuilder_free(bld); + return (int)i; + +on_error: + git_treebuilder_free(bld); + return -1; +} + +int git_tree__write_index( + git_oid *oid, git_index *index, git_repository *repo) +{ + int ret; + git_tree *tree; + git_str shared_buf = GIT_STR_INIT; + bool old_ignore_case = false; + + GIT_ASSERT_ARG(oid); + GIT_ASSERT_ARG(index); + GIT_ASSERT_ARG(repo); + + if (git_index_has_conflicts(index)) { + git_error_set(GIT_ERROR_INDEX, + "cannot create a tree from a not fully merged index."); + return GIT_EUNMERGED; + } + + if (index->tree != NULL && index->tree->entry_count >= 0) { + git_oid_cpy(oid, &index->tree->oid); + return 0; + } + + /* The tree cache didn't help us; we'll have to write + * out a tree. If the index is ignore_case, we must + * make it case-sensitive for the duration of the tree-write + * operation. */ + + if (index->ignore_case) { + old_ignore_case = true; + git_index__set_ignore_case(index, false); + } + + ret = write_tree(oid, repo, index, "", 0, &shared_buf); + git_str_dispose(&shared_buf); + + if (old_ignore_case) + git_index__set_ignore_case(index, true); + + index->tree = NULL; + + if (ret < 0) + return ret; + + git_pool_clear(&index->tree_pool); + + if ((ret = git_tree_lookup(&tree, repo, oid)) < 0) + return ret; + + /* Read the tree cache into the index */ + ret = git_tree_cache_read_tree(&index->tree, tree, index->oid_type, &index->tree_pool); + git_tree_free(tree); + + return ret; +} + +int git_treebuilder_new( + git_treebuilder **builder_p, + git_repository *repo, + const git_tree *source) +{ + git_treebuilder *bld; + size_t i; + + GIT_ASSERT_ARG(builder_p); + GIT_ASSERT_ARG(repo); + + bld = git__calloc(1, sizeof(git_treebuilder)); + GIT_ERROR_CHECK_ALLOC(bld); + + bld->repo = repo; + + if (git_strmap_new(&bld->map) < 0) { + git__free(bld); + return -1; + } + + if (source != NULL) { + git_tree_entry *entry_src; + + git_array_foreach(source->entries, i, entry_src) { + if (append_entry( + bld, entry_src->filename, + &entry_src->oid, + entry_src->attr, + false) < 0) + goto on_error; + } + } + + *builder_p = bld; + return 0; + +on_error: + git_treebuilder_free(bld); + return -1; +} + +int git_treebuilder_insert( + const git_tree_entry **entry_out, + git_treebuilder *bld, + const char *filename, + const git_oid *id, + git_filemode_t filemode) +{ + git_tree_entry *entry; + int error; + + GIT_ASSERT_ARG(bld); + GIT_ASSERT_ARG(id); + GIT_ASSERT_ARG(filename); + + if ((error = check_entry(bld->repo, filename, id, filemode)) < 0) + return error; + + if ((entry = git_strmap_get(bld->map, filename)) != NULL) { + git_oid_cpy(&entry->oid, id); + } else { + entry = alloc_entry(filename, strlen(filename), id); + GIT_ERROR_CHECK_ALLOC(entry); + + if ((error = git_strmap_set(bld->map, entry->filename, entry)) < 0) { + git_tree_entry_free(entry); + git_error_set(GIT_ERROR_TREE, "failed to insert %s", filename); + return -1; + } + } + + entry->attr = filemode; + + if (entry_out) + *entry_out = entry; + + return 0; +} + +static git_tree_entry *treebuilder_get(git_treebuilder *bld, const char *filename) +{ + GIT_ASSERT_ARG_WITH_RETVAL(bld, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(filename, NULL); + + return git_strmap_get(bld->map, filename); +} + +const git_tree_entry *git_treebuilder_get(git_treebuilder *bld, const char *filename) +{ + return treebuilder_get(bld, filename); +} + +int git_treebuilder_remove(git_treebuilder *bld, const char *filename) +{ + git_tree_entry *entry = treebuilder_get(bld, filename); + + if (entry == NULL) + return tree_error("failed to remove entry: file isn't in the tree", filename); + + git_strmap_delete(bld->map, filename); + git_tree_entry_free(entry); + + return 0; +} + +int git_treebuilder_write(git_oid *oid, git_treebuilder *bld) +{ + GIT_ASSERT_ARG(oid); + GIT_ASSERT_ARG(bld); + + return git_treebuilder__write_with_buffer(oid, bld, &bld->write_cache); +} + +int git_treebuilder_filter( + git_treebuilder *bld, + git_treebuilder_filter_cb filter, + void *payload) +{ + const char *filename; + git_tree_entry *entry; + + GIT_ASSERT_ARG(bld); + GIT_ASSERT_ARG(filter); + + git_strmap_foreach(bld->map, filename, entry, { + if (filter(entry, payload)) { + git_strmap_delete(bld->map, filename); + git_tree_entry_free(entry); + } + }); + + return 0; +} + +int git_treebuilder_clear(git_treebuilder *bld) +{ + git_tree_entry *e; + + GIT_ASSERT_ARG(bld); + + git_strmap_foreach_value(bld->map, e, git_tree_entry_free(e)); + git_strmap_clear(bld->map); + + return 0; +} + +void git_treebuilder_free(git_treebuilder *bld) +{ + if (bld == NULL) + return; + + git_str_dispose(&bld->write_cache); + git_treebuilder_clear(bld); + git_strmap_free(bld->map); + git__free(bld); +} + +static size_t subpath_len(const char *path) +{ + const char *slash_pos = strchr(path, '/'); + if (slash_pos == NULL) + return strlen(path); + + return slash_pos - path; +} + +int git_tree_entry_bypath( + git_tree_entry **entry_out, + const git_tree *root, + const char *path) +{ + int error = 0; + git_tree *subtree; + const git_tree_entry *entry; + size_t filename_len; + + /* Find how long is the current path component (i.e. + * the filename between two slashes */ + filename_len = subpath_len(path); + + if (filename_len == 0) { + git_error_set(GIT_ERROR_TREE, "invalid tree path given"); + return GIT_ENOTFOUND; + } + + entry = entry_fromname(root, path, filename_len); + + if (entry == NULL) { + git_error_set(GIT_ERROR_TREE, + "the path '%.*s' does not exist in the given tree", (int) filename_len, path); + return GIT_ENOTFOUND; + } + + switch (path[filename_len]) { + case '/': + /* If there are more components in the path... + * then this entry *must* be a tree */ + if (!git_tree_entry__is_tree(entry)) { + git_error_set(GIT_ERROR_TREE, + "the path '%.*s' exists but is not a tree", (int) filename_len, path); + return GIT_ENOTFOUND; + } + + /* If there's only a slash left in the path, we + * return the current entry; otherwise, we keep + * walking down the path */ + if (path[filename_len + 1] != '\0') + break; + /* fall through */ + case '\0': + /* If there are no more components in the path, return + * this entry */ + return git_tree_entry_dup(entry_out, entry); + } + + if (git_tree_lookup(&subtree, root->object.repo, &entry->oid) < 0) + return -1; + + error = git_tree_entry_bypath( + entry_out, + subtree, + path + filename_len + 1 + ); + + git_tree_free(subtree); + return error; +} + +static int tree_walk( + const git_tree *tree, + git_treewalk_cb callback, + git_str *path, + void *payload, + bool preorder) +{ + int error = 0; + size_t i; + const git_tree_entry *entry; + + git_array_foreach(tree->entries, i, entry) { + if (preorder) { + error = callback(path->ptr, entry, payload); + if (error < 0) { /* negative value stops iteration */ + git_error_set_after_callback_function(error, "git_tree_walk"); + break; + } + if (error > 0) { /* positive value skips this entry */ + error = 0; + continue; + } + } + + if (git_tree_entry__is_tree(entry)) { + git_tree *subtree; + size_t path_len = git_str_len(path); + + error = git_tree_lookup(&subtree, tree->object.repo, &entry->oid); + if (error < 0) + break; + + /* append the next entry to the path */ + git_str_puts(path, entry->filename); + git_str_putc(path, '/'); + + if (git_str_oom(path)) + error = -1; + else + error = tree_walk(subtree, callback, path, payload, preorder); + + git_tree_free(subtree); + if (error != 0) + break; + + git_str_truncate(path, path_len); + } + + if (!preorder) { + error = callback(path->ptr, entry, payload); + if (error < 0) { /* negative value stops iteration */ + git_error_set_after_callback_function(error, "git_tree_walk"); + break; + } + error = 0; + } + } + + return error; +} + +int git_tree_walk( + const git_tree *tree, + git_treewalk_mode mode, + git_treewalk_cb callback, + void *payload) +{ + int error = 0; + git_str root_path = GIT_STR_INIT; + + if (mode != GIT_TREEWALK_POST && mode != GIT_TREEWALK_PRE) { + git_error_set(GIT_ERROR_INVALID, "invalid walking mode for tree walk"); + return -1; + } + + error = tree_walk( + tree, callback, &root_path, payload, (mode == GIT_TREEWALK_PRE)); + + git_str_dispose(&root_path); + + return error; +} + +static int compare_entries(const void *_a, const void *_b) +{ + const git_tree_update *a = (git_tree_update *) _a; + const git_tree_update *b = (git_tree_update *) _b; + + return strcmp(a->path, b->path); +} + +static int on_dup_entry(void **old, void *new) +{ + GIT_UNUSED(old); GIT_UNUSED(new); + + git_error_set(GIT_ERROR_TREE, "duplicate entries given for update"); + return -1; +} + +/* + * We keep the previous tree and the new one at each level of the + * stack. When we leave a level we're done with that tree and we can + * write it out to the odb. + */ +typedef struct { + git_treebuilder *bld; + git_tree *tree; + char *name; +} tree_stack_entry; + +/** Count how many slashes (i.e. path components) there are in this string */ +GIT_INLINE(size_t) count_slashes(const char *path) +{ + size_t count = 0; + const char *slash; + + while ((slash = strchr(path, '/')) != NULL) { + count++; + path = slash + 1; + } + + return count; +} + +static bool next_component(git_str *out, const char *in) +{ + const char *slash = strchr(in, '/'); + + git_str_clear(out); + + if (slash) + git_str_put(out, in, slash - in); + + return !!slash; +} + +static int create_popped_tree(tree_stack_entry *current, tree_stack_entry *popped, git_str *component) +{ + int error; + git_oid new_tree; + + git_tree_free(popped->tree); + + /* If the tree would be empty, remove it from the one higher up */ + if (git_treebuilder_entrycount(popped->bld) == 0) { + git_treebuilder_free(popped->bld); + error = git_treebuilder_remove(current->bld, popped->name); + git__free(popped->name); + return error; + } + + error = git_treebuilder_write(&new_tree, popped->bld); + git_treebuilder_free(popped->bld); + + if (error < 0) { + git__free(popped->name); + return error; + } + + /* We've written out the tree, now we have to put the new value into its parent */ + git_str_clear(component); + git_str_puts(component, popped->name); + git__free(popped->name); + + GIT_ERROR_CHECK_ALLOC(component->ptr); + + /* Error out if this would create a D/F conflict in this update */ + if (current->tree) { + const git_tree_entry *to_replace; + to_replace = git_tree_entry_byname(current->tree, component->ptr); + if (to_replace && git_tree_entry_type(to_replace) != GIT_OBJECT_TREE) { + git_error_set(GIT_ERROR_TREE, "D/F conflict when updating tree"); + return -1; + } + } + + return git_treebuilder_insert(NULL, current->bld, component->ptr, &new_tree, GIT_FILEMODE_TREE); +} + +int git_tree_create_updated(git_oid *out, git_repository *repo, git_tree *baseline, size_t nupdates, const git_tree_update *updates) +{ + git_array_t(tree_stack_entry) stack = GIT_ARRAY_INIT; + tree_stack_entry *root_elem; + git_vector entries; + int error; + size_t i; + git_str component = GIT_STR_INIT; + + if ((error = git_vector_init(&entries, nupdates, compare_entries)) < 0) + return error; + + /* Sort the entries for treversal */ + for (i = 0 ; i < nupdates; i++) { + if ((error = git_vector_insert_sorted(&entries, (void *) &updates[i], on_dup_entry)) < 0) + goto cleanup; + } + + root_elem = git_array_alloc(stack); + GIT_ERROR_CHECK_ALLOC(root_elem); + memset(root_elem, 0, sizeof(*root_elem)); + + if (baseline && (error = git_tree_dup(&root_elem->tree, baseline)) < 0) + goto cleanup; + + if ((error = git_treebuilder_new(&root_elem->bld, repo, root_elem->tree)) < 0) + goto cleanup; + + for (i = 0; i < nupdates; i++) { + const git_tree_update *last_update = i == 0 ? NULL : git_vector_get(&entries, i-1); + const git_tree_update *update = git_vector_get(&entries, i); + size_t common_prefix = 0, steps_up, j; + const char *path; + + /* Figure out how much we need to change from the previous tree */ + if (last_update) + common_prefix = git_fs_path_common_dirlen(last_update->path, update->path); + + /* + * The entries are sorted, so when we find we're no + * longer in the same directory, we need to abandon + * the old tree (steps up) and dive down to the next + * one. + */ + steps_up = last_update == NULL ? 0 : count_slashes(&last_update->path[common_prefix]); + + for (j = 0; j < steps_up; j++) { + tree_stack_entry *current, *popped = git_array_pop(stack); + GIT_ASSERT(popped); + + current = git_array_last(stack); + GIT_ASSERT(current); + + if ((error = create_popped_tree(current, popped, &component)) < 0) + goto cleanup; + } + + /* Now that we've created the trees we popped from the stack, let's go back down */ + path = &update->path[common_prefix]; + while (next_component(&component, path)) { + tree_stack_entry *last, *new_entry; + const git_tree_entry *entry; + + last = git_array_last(stack); + entry = last->tree ? git_tree_entry_byname(last->tree, component.ptr) : NULL; + if (!entry) + entry = treebuilder_get(last->bld, component.ptr); + + if (entry && git_tree_entry_type(entry) != GIT_OBJECT_TREE) { + git_error_set(GIT_ERROR_TREE, "D/F conflict when updating tree"); + error = -1; + goto cleanup; + } + + new_entry = git_array_alloc(stack); + GIT_ERROR_CHECK_ALLOC(new_entry); + memset(new_entry, 0, sizeof(*new_entry)); + + new_entry->tree = NULL; + if (entry && (error = git_tree_lookup(&new_entry->tree, repo, git_tree_entry_id(entry))) < 0) + goto cleanup; + + if ((error = git_treebuilder_new(&new_entry->bld, repo, new_entry->tree)) < 0) + goto cleanup; + + new_entry->name = git__strdup(component.ptr); + GIT_ERROR_CHECK_ALLOC(new_entry->name); + + /* Get to the start of the next component */ + path += component.size + 1; + } + + /* After all that, we're finally at the place where we want to perform the update */ + switch (update->action) { + case GIT_TREE_UPDATE_UPSERT: + { + /* Make sure we're replacing something of the same type */ + tree_stack_entry *last = git_array_last(stack); + char *basename = git_fs_path_basename(update->path); + const git_tree_entry *e = git_treebuilder_get(last->bld, basename); + if (e && git_tree_entry_type(e) != git_object__type_from_filemode(update->filemode)) { + git__free(basename); + git_error_set(GIT_ERROR_TREE, "cannot replace '%s' with '%s' at '%s'", + git_object_type2string(git_tree_entry_type(e)), + git_object_type2string(git_object__type_from_filemode(update->filemode)), + update->path); + error = -1; + goto cleanup; + } + + error = git_treebuilder_insert(NULL, last->bld, basename, &update->id, update->filemode); + git__free(basename); + break; + } + case GIT_TREE_UPDATE_REMOVE: + { + tree_stack_entry *last = git_array_last(stack); + char *basename = git_fs_path_basename(update->path); + error = git_treebuilder_remove(last->bld, basename); + git__free(basename); + break; + } + default: + git_error_set(GIT_ERROR_TREE, "unknown action for update"); + error = -1; + goto cleanup; + } + + if (error < 0) + goto cleanup; + } + + /* We're done, go up the stack again and write out the tree */ + { + tree_stack_entry *current = NULL, *popped = NULL; + while ((popped = git_array_pop(stack)) != NULL) { + current = git_array_last(stack); + /* We've reached the top, current is the root tree */ + if (!current) + break; + + if ((error = create_popped_tree(current, popped, &component)) < 0) + goto cleanup; + } + + /* Write out the root tree */ + git__free(popped->name); + git_tree_free(popped->tree); + + error = git_treebuilder_write(out, popped->bld); + git_treebuilder_free(popped->bld); + if (error < 0) + goto cleanup; + } + +cleanup: + { + tree_stack_entry *e; + while ((e = git_array_pop(stack)) != NULL) { + git_treebuilder_free(e->bld); + git_tree_free(e->tree); + git__free(e->name); + } + } + + git_str_dispose(&component); + git_array_clear(stack); + git_vector_free(&entries); + return error; +} + +/* Deprecated Functions */ + +#ifndef GIT_DEPRECATE_HARD + +int git_treebuilder_write_with_buffer(git_oid *oid, git_treebuilder *bld, git_buf *buf) +{ + GIT_UNUSED(buf); + + return git_treebuilder_write(oid, bld); +} + +#endif diff --git a/src/libgit2/tree.h b/src/libgit2/tree.h new file mode 100644 index 0000000..5088450 --- /dev/null +++ b/src/libgit2/tree.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_tree_h__ +#define INCLUDE_tree_h__ + +#include "common.h" + +#include "git2/tree.h" +#include "repository.h" +#include "odb.h" +#include "vector.h" +#include "strmap.h" +#include "pool.h" + +struct git_tree_entry { + uint16_t attr; + uint16_t filename_len; + git_oid oid; + const char *filename; +}; + +struct git_tree { + git_object object; + git_odb_object *odb_obj; + git_array_t(git_tree_entry) entries; +}; + +struct git_treebuilder { + git_repository *repo; + git_strmap *map; + git_str write_cache; +}; + +GIT_INLINE(bool) git_tree_entry__is_tree(const struct git_tree_entry *e) +{ + return (S_ISDIR(e->attr) && !S_ISGITLINK(e->attr)); +} + +void git_tree__free(void *tree); +int git_tree__parse(void *tree, git_odb_object *obj, git_oid_t oid_type); +int git_tree__parse_raw(void *_tree, const char *data, size_t size, git_oid_t oid_type); + +/** + * Write a tree to the given repository + */ +int git_tree__write_index( + git_oid *oid, git_index *index, git_repository *repo); + +/** + * Obsolete mode kept for compatibility reasons + */ +#define GIT_FILEMODE_BLOB_GROUP_WRITABLE 0100664 + +#endif diff --git a/src/libgit2/userdiff.h b/src/libgit2/userdiff.h new file mode 100644 index 0000000..c9a80d7 --- /dev/null +++ b/src/libgit2/userdiff.h @@ -0,0 +1,210 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_userdiff_h__ +#define INCLUDE_userdiff_h__ + +#include "regexp.h" + +/* + * This file isolates the built in diff driver function name patterns. + * Most of these patterns are taken from Git (with permission from the + * original authors for relicensing to libgit2). + */ + +typedef struct { + const char *name; + const char *fns; + const char *words; + int flags; +} git_diff_driver_definition; + +#define WORD_DEFAULT "|[^[:space:]]|[\xc0-\xff][\x80-\xbf]+" + +/* + * These builtin driver definition macros have same signature as in core + * git userdiff.c so that the data can be extracted verbatim + */ +#define PATTERNS(NAME, FN_PATS, WORD_PAT) \ + { NAME, FN_PATS, WORD_PAT WORD_DEFAULT, 0 } +#define IPATTERN(NAME, FN_PATS, WORD_PAT) \ + { NAME, FN_PATS, WORD_PAT WORD_DEFAULT, GIT_REGEXP_ICASE } + +/* + * The table of diff driver patterns + * + * Function name patterns are a list of newline separated patterns that + * match a function declaration (i.e. the line you want in the hunk header), + * or a negative pattern prefixed with a '!' to reject a pattern (such as + * rejecting goto labels in C code). + * + * Word boundary patterns are just a simple pattern that will be OR'ed with + * the default value above (i.e. whitespace or non-ASCII characters). + */ +static git_diff_driver_definition builtin_defs[] = { + +IPATTERN("ada", + "!^(.*[ \t])?(is[ \t]+new|renames|is[ \t]+separate)([ \t].*)?$\n" + "!^[ \t]*with[ \t].*$\n" + "^[ \t]*((procedure|function)[ \t]+.*)$\n" + "^[ \t]*((package|protected|task)[ \t]+.*)$", + /* -- */ + "[a-zA-Z][a-zA-Z0-9_]*" + "|[-+]?[0-9][0-9#_.aAbBcCdDeEfF]*([eE][+-]?[0-9_]+)?" + "|=>|\\.\\.|\\*\\*|:=|/=|>=|<=|<<|>>|<>"), + +IPATTERN("fortran", + "!^([C*]|[ \t]*!)\n" + "!^[ \t]*MODULE[ \t]+PROCEDURE[ \t]\n" + "^[ \t]*((END[ \t]+)?(PROGRAM|MODULE|BLOCK[ \t]+DATA" + "|([^'\" \t]+[ \t]+)*(SUBROUTINE|FUNCTION))[ \t]+[A-Z].*)$", + /* -- */ + "[a-zA-Z][a-zA-Z0-9_]*" + "|\\.([Ee][Qq]|[Nn][Ee]|[Gg][TtEe]|[Ll][TtEe]|[Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee]|[Aa][Nn][Dd]|[Oo][Rr]|[Nn]?[Ee][Qq][Vv]|[Nn][Oo][Tt])\\." + /* numbers and format statements like 2E14.4, or ES12.6, 9X. + * Don't worry about format statements without leading digits since + * they would have been matched above as a variable anyway. */ + "|[-+]?[0-9.]+([AaIiDdEeFfLlTtXx][Ss]?[-+]?[0-9.]*)?(_[a-zA-Z0-9][a-zA-Z0-9_]*)?" + "|//|\\*\\*|::|[/<>=]="), + +PATTERNS("html", "^[ \t]*(<[Hh][1-6][ \t].*>.*)$", + "[^<>= \t]+"), + +PATTERNS("java", + "!^[ \t]*(catch|do|for|if|instanceof|new|return|switch|throw|while)\n" + "^[ \t]*(([A-Za-z_][A-Za-z_0-9]*[ \t]+)+[A-Za-z_][A-Za-z_0-9]*[ \t]*\\([^;]*)$", + /* -- */ + "[a-zA-Z_][a-zA-Z0-9_]*" + "|[-+0-9.e]+[fFlL]?|0[xXbB]?[0-9a-fA-F]+[lL]?" + "|[-+*/<>%&^|=!]=" + "|--|\\+\\+|<<=?|>>>?=?|&&|\\|\\|"), + +PATTERNS("matlab", + "^[[:space:]]*((classdef|function)[[:space:]].*)$|^%%[[:space:]].*$", + "[a-zA-Z_][a-zA-Z0-9_]*|[-+0-9.e]+|[=~<>]=|\\.[*/\\^']|\\|\\||&&"), + +PATTERNS("objc", + /* Negate C statements that can look like functions */ + "!^[ \t]*(do|for|if|else|return|switch|while)\n" + /* Objective-C methods */ + "^[ \t]*([-+][ \t]*\\([ \t]*[A-Za-z_][A-Za-z_0-9* \t]*\\)[ \t]*[A-Za-z_].*)$\n" + /* C functions */ + "^[ \t]*(([A-Za-z_][A-Za-z_0-9]*[ \t]+)+[A-Za-z_][A-Za-z_0-9]*[ \t]*\\([^;]*)$\n" + /* Objective-C class/protocol definitions */ + "^(@(implementation|interface|protocol)[ \t].*)$", + /* -- */ + "[a-zA-Z_][a-zA-Z0-9_]*" + "|[-+0-9.e]+[fFlL]?|0[xXbB]?[0-9a-fA-F]+[lL]?" + "|[-+*/<>%&^|=!]=|--|\\+\\+|<<=?|>>=?|&&|\\|\\||::|->"), + +PATTERNS("pascal", + "^(((class[ \t]+)?(procedure|function)|constructor|destructor|interface|" + "implementation|initialization|finalization)[ \t]*.*)$" + "\n" + "^(.*=[ \t]*(class|record).*)$", + /* -- */ + "[a-zA-Z_][a-zA-Z0-9_]*" + "|[-+0-9.e]+|0[xXbB]?[0-9a-fA-F]+" + "|<>|<=|>=|:=|\\.\\."), + +PATTERNS("perl", + "^package .*\n" + "^sub [[:alnum:]_':]+[ \t]*" + "(\\([^)]*\\)[ \t]*)?" /* prototype */ + /* + * Attributes. A regex can't count nested parentheses, + * so just slurp up whatever we see, taking care not + * to accept lines like "sub foo; # defined elsewhere". + * + * An attribute could contain a semicolon, but at that + * point it seems reasonable enough to give up. + */ + "(:[^;#]*)?" + "(\\{[ \t]*)?" /* brace can come here or on the next line */ + "(#.*)?$\n" /* comment */ + "^(BEGIN|END|INIT|CHECK|UNITCHECK|AUTOLOAD|DESTROY)[ \t]*" + "(\\{[ \t]*)?" /* brace can come here or on the next line */ + "(#.*)?$\n" + "^=head[0-9] .*", /* POD */ + /* -- */ + "[[:alpha:]_'][[:alnum:]_']*" + "|0[xb]?[0-9a-fA-F_]*" + /* taking care not to interpret 3..5 as (3.)(.5) */ + "|[0-9a-fA-F_]+(\\.[0-9a-fA-F_]+)?([eE][-+]?[0-9_]+)?" + "|=>|-[rwxoRWXOezsfdlpSugkbctTBMAC>]|~~|::" + "|&&=|\\|\\|=|//=|\\*\\*=" + "|&&|\\|\\||//|\\+\\+|--|\\*\\*|\\.\\.\\.?" + "|[-+*/%.^&<>=!|]=" + "|=~|!~" + "|<<|<>|<=>|>>"), + +PATTERNS("python", "^[ \t]*((class|def)[ \t].*)$", + /* -- */ + "[a-zA-Z_][a-zA-Z0-9_]*" + "|[-+0-9.e]+[jJlL]?|0[xX]?[0-9a-fA-F]+[lL]?" + "|[-+*/<>%&^|=!]=|//=?|<<=?|>>=?|\\*\\*=?"), + +PATTERNS("ruby", "^[ \t]*((class|module|def)[ \t].*)$", + /* -- */ + "(@|@@|\\$)?[a-zA-Z_][a-zA-Z0-9_]*" + "|[-+0-9.e]+|0[xXbB]?[0-9a-fA-F]+|\\?(\\\\C-)?(\\\\M-)?." + "|//=?|[-+*/<>%&^|=!]=|<<=?|>>=?|===|\\.{1,3}|::|[!=]~"), + +PATTERNS("bibtex", "(@[a-zA-Z]{1,}[ \t]*\\{{0,1}[ \t]*[^ \t\"@',\\#}{~%]*).*$", + "[={}\"]|[^={}\" \t]+"), + +PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$", + "\\\\[a-zA-Z@]+|\\\\.|[a-zA-Z0-9\x80-\xff]+"), + +PATTERNS("cpp", + /* Jump targets or access declarations */ + "!^[ \t]*[A-Za-z_][A-Za-z_0-9]*:[[:space:]]*($|/[/*])\n" + /* functions/methods, variables, and compounds at top level */ + "^((::[[:space:]]*)?[A-Za-z_].*)$", + /* -- */ + "[a-zA-Z_][a-zA-Z0-9_]*" + "|[-+0-9.e]+[fFlL]?|0[xXbB]?[0-9a-fA-F]+[lLuU]*" + "|[-+*/<>%&^|=!]=|--|\\+\\+|<<=?|>>=?|&&|\\|\\||::|->\\*?|\\.\\*"), + +PATTERNS("csharp", + /* Keywords */ + "!^[ \t]*(do|while|for|if|else|instanceof|new|return|switch|case|throw|catch|using)\n" + /* Methods and constructors */ + "^[ \t]*(((static|public|internal|private|protected|new|virtual|sealed|override|unsafe)[ \t]+)*[][<>@.~_[:alnum:]]+[ \t]+[<>@._[:alnum:]]+[ \t]*\\(.*\\))[ \t]*$\n" + /* Properties */ + "^[ \t]*(((static|public|internal|private|protected|new|virtual|sealed|override|unsafe)[ \t]+)*[][<>@.~_[:alnum:]]+[ \t]+[@._[:alnum:]]+)[ \t]*$\n" + /* Type definitions */ + "^[ \t]*(((static|public|internal|private|protected|new|unsafe|sealed|abstract|partial)[ \t]+)*(class|enum|interface|struct)[ \t]+.*)$\n" + /* Namespace */ + "^[ \t]*(namespace[ \t]+.*)$", + /* -- */ + "[a-zA-Z_][a-zA-Z0-9_]*" + "|[-+0-9.e]+[fFlL]?|0[xXbB]?[0-9a-fA-F]+[lL]?" + "|[-+*/<>%&^|=!]=|--|\\+\\+|<<=?|>>=?|&&|\\|\\||::|->"), + +PATTERNS("php", + "^[ \t]*(((public|private|protected|static|final)[ \t]+)*((class|function)[ \t].*))$", + /* -- */ + "[a-zA-Z_][a-zA-Z0-9_]*" + "|[-+0-9.e]+[fFlL]?|0[xX]?[0-9a-fA-F]+[lL]?" + "|[-+*/<>%&^|=!]=|--|\\+\\+|<<=?|>>=?|&&|\\|\\||::|->"), + +PATTERNS("javascript", + "([a-zA-Z_$][a-zA-Z0-9_$]*(\\.[a-zA-Z0-9_$]+)*[ \t]*=[ \t]*function([ \t][a-zA-Z_$][a-zA-Z0-9_$]*)?[^\\{]*)\n" + "([a-zA-Z_$][a-zA-Z0-9_$]*[ \t]*:[ \t]*function([ \t][a-zA-Z_$][a-zA-Z0-9_$]*)?[^\\{]*)\n" + "[^a-zA-Z0-9_\\$](function([ \t][a-zA-Z_$][a-zA-Z0-9_$]*)?[^\\{]*)", + /* -- */ + "[a-zA-Z_][a-zA-Z0-9_]*" + "|[-+0-9.e]+[fFlL]?|0[xX]?[0-9a-fA-F]+[lL]?" + "|[-+*/<>%&^|=!]=|--|\\+\\+|<<=?|>>=?|&&|\\|\\||::|->"), +}; + +#undef IPATTERN +#undef PATTERNS +#undef WORD_DEFAULT + +#endif + diff --git a/src/libgit2/worktree.c b/src/libgit2/worktree.c new file mode 100644 index 0000000..a878634 --- /dev/null +++ b/src/libgit2/worktree.c @@ -0,0 +1,672 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "worktree.h" + +#include "buf.h" +#include "repository.h" +#include "path.h" + +#include "git2/branch.h" +#include "git2/commit.h" +#include "git2/worktree.h" + +static bool is_worktree_dir(const char *dir) +{ + git_str buf = GIT_STR_INIT; + int error; + + if (git_str_sets(&buf, dir) < 0) + return -1; + + error = git_fs_path_contains_file(&buf, "commondir") + && git_fs_path_contains_file(&buf, "gitdir") + && git_fs_path_contains_file(&buf, "HEAD"); + + git_str_dispose(&buf); + return error; +} + +int git_worktree_list(git_strarray *wts, git_repository *repo) +{ + git_vector worktrees = GIT_VECTOR_INIT; + git_str path = GIT_STR_INIT; + char *worktree; + size_t i, len; + int error; + + GIT_ASSERT_ARG(wts); + GIT_ASSERT_ARG(repo); + + wts->count = 0; + wts->strings = NULL; + + if ((error = git_str_joinpath(&path, repo->commondir, "worktrees/")) < 0) + goto exit; + if (!git_fs_path_exists(path.ptr) || git_fs_path_is_empty_dir(path.ptr)) + goto exit; + if ((error = git_fs_path_dirload(&worktrees, path.ptr, path.size, 0x0)) < 0) + goto exit; + + len = path.size; + + git_vector_foreach(&worktrees, i, worktree) { + git_str_truncate(&path, len); + git_str_puts(&path, worktree); + + if (!is_worktree_dir(path.ptr)) { + git_vector_remove(&worktrees, i); + git__free(worktree); + } + } + + wts->strings = (char **)git_vector_detach(&wts->count, NULL, &worktrees); + +exit: + git_str_dispose(&path); + + return error; +} + +char *git_worktree__read_link(const char *base, const char *file) +{ + git_str path = GIT_STR_INIT, buf = GIT_STR_INIT; + + GIT_ASSERT_ARG_WITH_RETVAL(base, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(file, NULL); + + if (git_str_joinpath(&path, base, file) < 0) + goto err; + if (git_futils_readbuffer(&buf, path.ptr) < 0) + goto err; + git_str_dispose(&path); + + git_str_rtrim(&buf); + + if (!git_fs_path_is_relative(buf.ptr)) + return git_str_detach(&buf); + + if (git_str_sets(&path, base) < 0) + goto err; + if (git_fs_path_apply_relative(&path, buf.ptr) < 0) + goto err; + git_str_dispose(&buf); + + return git_str_detach(&path); + +err: + git_str_dispose(&buf); + git_str_dispose(&path); + + return NULL; +} + +static int write_wtfile(const char *base, const char *file, const git_str *buf) +{ + git_str path = GIT_STR_INIT; + int err; + + GIT_ASSERT_ARG(base); + GIT_ASSERT_ARG(file); + GIT_ASSERT_ARG(buf); + + if ((err = git_str_joinpath(&path, base, file)) < 0) + goto out; + + if ((err = git_futils_writebuffer(buf, path.ptr, O_CREAT|O_EXCL|O_WRONLY, 0644)) < 0) + goto out; + +out: + git_str_dispose(&path); + + return err; +} + +static int open_worktree_dir(git_worktree **out, const char *parent, const char *dir, const char *name) +{ + git_str gitdir = GIT_STR_INIT; + git_worktree *wt = NULL; + int error = 0; + + if (!is_worktree_dir(dir)) { + error = -1; + goto out; + } + + if ((error = git_path_validate_length(NULL, dir)) < 0) + goto out; + + if ((wt = git__calloc(1, sizeof(*wt))) == NULL) { + error = -1; + goto out; + } + + if ((wt->name = git__strdup(name)) == NULL || + (wt->commondir_path = git_worktree__read_link(dir, "commondir")) == NULL || + (wt->gitlink_path = git_worktree__read_link(dir, "gitdir")) == NULL || + (parent && (wt->parent_path = git__strdup(parent)) == NULL) || + (wt->worktree_path = git_fs_path_dirname(wt->gitlink_path)) == NULL) { + error = -1; + goto out; + } + + if ((error = git_fs_path_prettify_dir(&gitdir, dir, NULL)) < 0) + goto out; + wt->gitdir_path = git_str_detach(&gitdir); + + if ((error = git_worktree_is_locked(NULL, wt)) < 0) + goto out; + wt->locked = !!error; + error = 0; + + *out = wt; + +out: + if (error) + git_worktree_free(wt); + git_str_dispose(&gitdir); + + return error; +} + +int git_worktree_lookup(git_worktree **out, git_repository *repo, const char *name) +{ + git_str path = GIT_STR_INIT; + git_worktree *wt = NULL; + int error; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + + *out = NULL; + + if ((error = git_str_join3(&path, '/', repo->commondir, "worktrees", name)) < 0) + goto out; + + if (!git_fs_path_isdir(path.ptr)) { + error = GIT_ENOTFOUND; + goto out; + } + + if ((error = (open_worktree_dir(out, git_repository_workdir(repo), path.ptr, name))) < 0) + goto out; + +out: + git_str_dispose(&path); + + if (error) + git_worktree_free(wt); + + return error; +} + +int git_worktree_open_from_repository(git_worktree **out, git_repository *repo) +{ + git_str parent = GIT_STR_INIT; + const char *gitdir, *commondir; + char *name = NULL; + int error = 0; + + if (!git_repository_is_worktree(repo)) { + git_error_set(GIT_ERROR_WORKTREE, "cannot open worktree of a non-worktree repo"); + error = -1; + goto out; + } + + gitdir = git_repository_path(repo); + commondir = git_repository_commondir(repo); + + if ((error = git_fs_path_prettify_dir(&parent, "..", commondir)) < 0) + goto out; + + /* The name is defined by the last component in '.git/worktree/%s' */ + name = git_fs_path_basename(gitdir); + + if ((error = open_worktree_dir(out, parent.ptr, gitdir, name)) < 0) + goto out; + +out: + git__free(name); + git_str_dispose(&parent); + + return error; +} + +void git_worktree_free(git_worktree *wt) +{ + if (!wt) + return; + + git__free(wt->commondir_path); + git__free(wt->worktree_path); + git__free(wt->gitlink_path); + git__free(wt->gitdir_path); + git__free(wt->parent_path); + git__free(wt->name); + git__free(wt); +} + +int git_worktree_validate(const git_worktree *wt) +{ + GIT_ASSERT_ARG(wt); + + if (!is_worktree_dir(wt->gitdir_path)) { + git_error_set(GIT_ERROR_WORKTREE, + "worktree gitdir ('%s') is not valid", + wt->gitlink_path); + return GIT_ERROR; + } + + if (wt->parent_path && !git_fs_path_exists(wt->parent_path)) { + git_error_set(GIT_ERROR_WORKTREE, + "worktree parent directory ('%s') does not exist ", + wt->parent_path); + return GIT_ERROR; + } + + if (!git_fs_path_exists(wt->commondir_path)) { + git_error_set(GIT_ERROR_WORKTREE, + "worktree common directory ('%s') does not exist ", + wt->commondir_path); + return GIT_ERROR; + } + + if (!git_fs_path_exists(wt->worktree_path)) { + git_error_set(GIT_ERROR_WORKTREE, + "worktree directory '%s' does not exist", + wt->worktree_path); + return GIT_ERROR; + } + + return 0; +} + +int git_worktree_add_options_init(git_worktree_add_options *opts, + unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE(opts, version, + git_worktree_add_options, GIT_WORKTREE_ADD_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_worktree_add_init_options(git_worktree_add_options *opts, + unsigned int version) +{ + return git_worktree_add_options_init(opts, version); +} +#endif + +int git_worktree_add(git_worktree **out, git_repository *repo, + const char *name, const char *worktree, + const git_worktree_add_options *opts) +{ + git_str gitdir = GIT_STR_INIT, wddir = GIT_STR_INIT, buf = GIT_STR_INIT; + git_reference *ref = NULL, *head = NULL; + git_commit *commit = NULL; + git_repository *wt = NULL; + git_checkout_options coopts; + git_worktree_add_options wtopts = GIT_WORKTREE_ADD_OPTIONS_INIT; + int err; + + GIT_ERROR_CHECK_VERSION( + opts, GIT_WORKTREE_ADD_OPTIONS_VERSION, "git_worktree_add_options"); + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(name); + GIT_ASSERT_ARG(worktree); + + *out = NULL; + + if (opts) + memcpy(&wtopts, opts, sizeof(wtopts)); + + memcpy(&coopts, &wtopts.checkout_options, sizeof(coopts)); + + if (wtopts.ref) { + if (!git_reference_is_branch(wtopts.ref)) { + git_error_set(GIT_ERROR_WORKTREE, "reference is not a branch"); + err = -1; + goto out; + } + + if (git_branch_is_checked_out(wtopts.ref)) { + git_error_set(GIT_ERROR_WORKTREE, "reference is already checked out"); + err = -1; + goto out; + } + } + + /* Create gitdir directory ".git/worktrees/" */ + if ((err = git_str_joinpath(&gitdir, repo->commondir, "worktrees")) < 0) + goto out; + if (!git_fs_path_exists(gitdir.ptr)) + if ((err = git_futils_mkdir(gitdir.ptr, 0755, GIT_MKDIR_EXCL)) < 0) + goto out; + if ((err = git_str_joinpath(&gitdir, gitdir.ptr, name)) < 0) + goto out; + if ((err = git_futils_mkdir(gitdir.ptr, 0755, GIT_MKDIR_EXCL)) < 0) + goto out; + if ((err = git_fs_path_prettify_dir(&gitdir, gitdir.ptr, NULL)) < 0) + goto out; + + /* Create worktree work dir */ + if ((err = git_futils_mkdir(worktree, 0755, GIT_MKDIR_EXCL)) < 0) + goto out; + if ((err = git_fs_path_prettify_dir(&wddir, worktree, NULL)) < 0) + goto out; + + if (wtopts.lock) { + int fd; + + if ((err = git_str_joinpath(&buf, gitdir.ptr, "locked")) < 0) + goto out; + + if ((fd = p_creat(buf.ptr, 0644)) < 0) { + err = fd; + goto out; + } + + p_close(fd); + git_str_clear(&buf); + } + + /* Create worktree .git file */ + if ((err = git_str_printf(&buf, "gitdir: %s\n", gitdir.ptr)) < 0) + goto out; + if ((err = write_wtfile(wddir.ptr, ".git", &buf)) < 0) + goto out; + + /* Create gitdir files */ + if ((err = git_fs_path_prettify_dir(&buf, repo->commondir, NULL) < 0) + || (err = git_str_putc(&buf, '\n')) < 0 + || (err = write_wtfile(gitdir.ptr, "commondir", &buf)) < 0) + goto out; + if ((err = git_str_joinpath(&buf, wddir.ptr, ".git")) < 0 + || (err = git_str_putc(&buf, '\n')) < 0 + || (err = write_wtfile(gitdir.ptr, "gitdir", &buf)) < 0) + goto out; + + /* Set up worktree reference */ + if (wtopts.ref) { + if ((err = git_reference_dup(&ref, wtopts.ref)) < 0) + goto out; + } else { + if ((err = git_repository_head(&head, repo)) < 0) + goto out; + if ((err = git_commit_lookup(&commit, repo, &head->target.oid)) < 0) + goto out; + if ((err = git_branch_create(&ref, repo, name, commit, false)) < 0) + goto out; + } + + /* Set worktree's HEAD */ + if ((err = git_repository_create_head(gitdir.ptr, git_reference_name(ref))) < 0) + goto out; + if ((err = git_repository_open(&wt, wddir.ptr)) < 0) + goto out; + + /* Checkout worktree's HEAD */ + if ((err = git_checkout_head(wt, &coopts)) < 0) + goto out; + + /* Load result */ + if ((err = git_worktree_lookup(out, repo, name)) < 0) + goto out; + +out: + git_str_dispose(&gitdir); + git_str_dispose(&wddir); + git_str_dispose(&buf); + git_reference_free(ref); + git_reference_free(head); + git_commit_free(commit); + git_repository_free(wt); + + return err; +} + +int git_worktree_lock(git_worktree *wt, const char *reason) +{ + git_str buf = GIT_STR_INIT, path = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(wt); + + if ((error = git_worktree_is_locked(NULL, wt)) < 0) + goto out; + if (error) { + error = GIT_ELOCKED; + goto out; + } + + if ((error = git_str_joinpath(&path, wt->gitdir_path, "locked")) < 0) + goto out; + + if (reason) + git_str_attach_notowned(&buf, reason, strlen(reason)); + + if ((error = git_futils_writebuffer(&buf, path.ptr, O_CREAT|O_EXCL|O_WRONLY, 0644)) < 0) + goto out; + + wt->locked = 1; + +out: + git_str_dispose(&path); + + return error; +} + +int git_worktree_unlock(git_worktree *wt) +{ + git_str path = GIT_STR_INIT; + int error; + + GIT_ASSERT_ARG(wt); + + if ((error = git_worktree_is_locked(NULL, wt)) < 0) + return error; + if (!error) + return 1; + + if (git_str_joinpath(&path, wt->gitdir_path, "locked") < 0) + return -1; + + if (p_unlink(path.ptr) != 0) { + git_str_dispose(&path); + return -1; + } + + wt->locked = 0; + + git_str_dispose(&path); + + return 0; +} + +static int git_worktree__is_locked(git_str *reason, const git_worktree *wt) +{ + git_str path = GIT_STR_INIT; + int error, locked; + + GIT_ASSERT_ARG(wt); + + if (reason) + git_str_clear(reason); + + if ((error = git_str_joinpath(&path, wt->gitdir_path, "locked")) < 0) + goto out; + locked = git_fs_path_exists(path.ptr); + if (locked && reason && + (error = git_futils_readbuffer(reason, path.ptr)) < 0) + goto out; + + error = locked; +out: + git_str_dispose(&path); + + return error; +} + +int git_worktree_is_locked(git_buf *reason, const git_worktree *wt) +{ + git_str str = GIT_STR_INIT; + int error = 0; + + if (reason && (error = git_buf_tostr(&str, reason)) < 0) + return error; + + error = git_worktree__is_locked(reason ? &str : NULL, wt); + + if (error >= 0 && reason) { + if (git_buf_fromstr(reason, &str) < 0) + error = -1; + } + + git_str_dispose(&str); + return error; +} + +const char *git_worktree_name(const git_worktree *wt) +{ + GIT_ASSERT_ARG_WITH_RETVAL(wt, NULL); + return wt->name; +} + +const char *git_worktree_path(const git_worktree *wt) +{ + GIT_ASSERT_ARG_WITH_RETVAL(wt, NULL); + return wt->worktree_path; +} + +int git_worktree_prune_options_init( + git_worktree_prune_options *opts, + unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE(opts, version, + git_worktree_prune_options, GIT_WORKTREE_PRUNE_OPTIONS_INIT); + return 0; +} + +#ifndef GIT_DEPRECATE_HARD +int git_worktree_prune_init_options(git_worktree_prune_options *opts, + unsigned int version) +{ + return git_worktree_prune_options_init(opts, version); +} +#endif + +int git_worktree_is_prunable(git_worktree *wt, + git_worktree_prune_options *opts) +{ + git_worktree_prune_options popts = GIT_WORKTREE_PRUNE_OPTIONS_INIT; + git_str path = GIT_STR_INIT; + int ret = 0; + + GIT_ERROR_CHECK_VERSION( + opts, GIT_WORKTREE_PRUNE_OPTIONS_VERSION, + "git_worktree_prune_options"); + + if (opts) + memcpy(&popts, opts, sizeof(popts)); + + if ((popts.flags & GIT_WORKTREE_PRUNE_LOCKED) == 0) { + git_str reason = GIT_STR_INIT; + + if ((ret = git_worktree__is_locked(&reason, wt)) < 0) + goto out; + + if (ret) { + git_error_set(GIT_ERROR_WORKTREE, + "not pruning locked working tree: '%s'", + reason.size ? reason.ptr : "is locked"); + + git_str_dispose(&reason); + ret = 0; + goto out; + } + } + + if ((popts.flags & GIT_WORKTREE_PRUNE_VALID) == 0 && + git_worktree_validate(wt) == 0) { + git_error_set(GIT_ERROR_WORKTREE, "not pruning valid working tree"); + goto out; + } + + if ((ret = git_str_printf(&path, "%s/worktrees/%s", wt->commondir_path, wt->name) < 0)) + goto out; + + if (!git_fs_path_exists(path.ptr)) { + git_error_set(GIT_ERROR_WORKTREE, "worktree gitdir ('%s') does not exist", path.ptr); + goto out; + } + + ret = 1; + +out: + git_str_dispose(&path); + return ret; +} + +int git_worktree_prune(git_worktree *wt, + git_worktree_prune_options *opts) +{ + git_worktree_prune_options popts = GIT_WORKTREE_PRUNE_OPTIONS_INIT; + git_str path = GIT_STR_INIT; + char *wtpath; + int err; + + GIT_ERROR_CHECK_VERSION( + opts, GIT_WORKTREE_PRUNE_OPTIONS_VERSION, + "git_worktree_prune_options"); + + if (opts) + memcpy(&popts, opts, sizeof(popts)); + + if (!git_worktree_is_prunable(wt, &popts)) { + err = -1; + goto out; + } + + /* Delete gitdir in parent repository */ + if ((err = git_str_join3(&path, '/', wt->commondir_path, "worktrees", wt->name)) < 0) + goto out; + if (!git_fs_path_exists(path.ptr)) + { + git_error_set(GIT_ERROR_WORKTREE, "worktree gitdir '%s' does not exist", path.ptr); + err = -1; + goto out; + } + if ((err = git_futils_rmdir_r(path.ptr, NULL, GIT_RMDIR_REMOVE_FILES)) < 0) + goto out; + + /* Skip deletion of the actual working tree if it does + * not exist or deletion was not requested */ + if ((popts.flags & GIT_WORKTREE_PRUNE_WORKING_TREE) == 0 || + !git_fs_path_exists(wt->gitlink_path)) + { + goto out; + } + + if ((wtpath = git_fs_path_dirname(wt->gitlink_path)) == NULL) + goto out; + git_str_attach(&path, wtpath, 0); + if (!git_fs_path_exists(path.ptr)) + { + git_error_set(GIT_ERROR_WORKTREE, "working tree '%s' does not exist", path.ptr); + err = -1; + goto out; + } + if ((err = git_futils_rmdir_r(path.ptr, NULL, GIT_RMDIR_REMOVE_FILES)) < 0) + goto out; + +out: + git_str_dispose(&path); + + return err; +} diff --git a/src/libgit2/worktree.h b/src/libgit2/worktree.h new file mode 100644 index 0000000..587189f --- /dev/null +++ b/src/libgit2/worktree.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_worktree_h__ +#define INCLUDE_worktree_h__ + +#include "common.h" + +#include "git2/common.h" +#include "git2/worktree.h" + +struct git_worktree { + /* Name of the working tree. This is the name of the + * containing directory in the `$PARENT/.git/worktrees/` + * directory. */ + char *name; + + /* Path to the where the worktree lives in the filesystem */ + char *worktree_path; + /* Path to the .git file in the working tree's repository */ + char *gitlink_path; + /* Path to the .git directory inside the parent's + * worktrees directory */ + char *gitdir_path; + /* Path to the common directory contained in the parent + * repository */ + char *commondir_path; + /* Path to the parent's working directory */ + char *parent_path; + + unsigned int locked:1; +}; + +char *git_worktree__read_link(const char *base, const char *file); + +#endif diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt new file mode 100644 index 0000000..ee35eb9 --- /dev/null +++ b/src/util/CMakeLists.txt @@ -0,0 +1,79 @@ +# util: a shared library for common utility functions for libgit2 projects + +add_library(util OBJECT) +set_target_properties(util PROPERTIES C_STANDARD 90) +set_target_properties(util PROPERTIES C_EXTENSIONS OFF) + +configure_file(git2_features.h.in git2_features.h) + +set(UTIL_INCLUDES + "${PROJECT_BINARY_DIR}/src/util" + "${PROJECT_BINARY_DIR}/include" + "${PROJECT_SOURCE_DIR}/src/util" + "${PROJECT_SOURCE_DIR}/include") + +file(GLOB UTIL_SRC *.c *.h allocators/*.c allocators/*.h hash.h) +list(SORT UTIL_SRC) + +# +# Platform specific sources +# + +if(WIN32 AND NOT CYGWIN) + file(GLOB UTIL_SRC_OS win32/*.c win32/*.h) + list(SORT UTIL_SRC_OS) +elseif(NOT AMIGA) + file(GLOB UTIL_SRC_OS unix/*.c unix/*.h) + list(SORT UTIL_SRC_OS) +endif() + +# +# Hash backend selection +# + +if(USE_SHA1 STREQUAL "CollisionDetection") + file(GLOB UTIL_SRC_SHA1 hash/collisiondetect.* hash/sha1dc/*) + target_compile_definitions(util PRIVATE SHA1DC_NO_STANDARD_INCLUDES=1) + target_compile_definitions(util PRIVATE SHA1DC_CUSTOM_INCLUDE_SHA1_C=\"git2_util.h\") + target_compile_definitions(util PRIVATE SHA1DC_CUSTOM_INCLUDE_UBC_CHECK_C=\"git2_util.h\") +elseif(USE_SHA1 STREQUAL "OpenSSL" OR USE_SHA1 STREQUAL "OpenSSL-Dynamic") + add_definitions(-DOPENSSL_API_COMPAT=0x10100000L) + file(GLOB UTIL_SRC_SHA1 hash/openssl.*) +elseif(USE_SHA1 STREQUAL "CommonCrypto") + file(GLOB UTIL_SRC_SHA1 hash/common_crypto.*) +elseif(USE_SHA1 STREQUAL "mbedTLS") + file(GLOB UTIL_SRC_SHA1 hash/mbedtls.*) +elseif(USE_SHA1 STREQUAL "Win32") + file(GLOB UTIL_SRC_SHA1 hash/win32.*) +else() + message(FATAL_ERROR "Asked for unknown SHA1 backend: ${USE_SHA1}") +endif() + +list(SORT UTIL_SRC_SHA1) + +if(USE_SHA256 STREQUAL "Builtin") + file(GLOB UTIL_SRC_SHA256 hash/builtin.* hash/rfc6234/*) +elseif(USE_SHA256 STREQUAL "OpenSSL" OR USE_SHA256 STREQUAL "OpenSSL-Dynamic") + add_definitions(-DOPENSSL_API_COMPAT=0x10100000L) + file(GLOB UTIL_SRC_SHA256 hash/openssl.*) +elseif(USE_SHA256 STREQUAL "CommonCrypto") + file(GLOB UTIL_SRC_SHA256 hash/common_crypto.*) +elseif(USE_SHA256 STREQUAL "mbedTLS") + file(GLOB UTIL_SRC_SHA256 hash/mbedtls.*) +elseif(USE_SHA256 STREQUAL "Win32") + file(GLOB UTIL_SRC_SHA256 hash/win32.*) +else() + message(FATAL_ERROR "Asked for unknown SHA256 backend: ${USE_SHA256}") +endif() + +list(SORT UTIL_SRC_SHA256) + +# +# Build the library +# + +target_sources(util PRIVATE ${UTIL_SRC} ${UTIL_SRC_OS} ${UTIL_SRC_SHA1} ${UTIL_SRC_SHA256}) +ide_split_sources(util) + +target_include_directories(util PRIVATE ${UTIL_INCLUDES} ${LIBGIT2_DEPENDENCY_INCLUDES} PUBLIC ${libgit2_SOURCE_DIR}/include) +target_include_directories(util SYSTEM PRIVATE ${LIBGIT2_SYSTEM_INCLUDES}) diff --git a/src/util/alloc.c b/src/util/alloc.c new file mode 100644 index 0000000..6ec173d --- /dev/null +++ b/src/util/alloc.c @@ -0,0 +1,115 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "alloc.h" +#include "runtime.h" + +#include "allocators/failalloc.h" +#include "allocators/stdalloc.h" +#include "allocators/win32_leakcheck.h" + +/* Fail any allocation until git_libgit2_init is called. */ +git_allocator git__allocator = { + git_failalloc_malloc, + git_failalloc_realloc, + git_failalloc_free +}; + +void *git__calloc(size_t nelem, size_t elsize) +{ + size_t newsize; + void *ptr; + + if (GIT_MULTIPLY_SIZET_OVERFLOW(&newsize, nelem, elsize)) + return NULL; + + if ((ptr = git__malloc(newsize))) + memset(ptr, 0, newsize); + + return ptr; +} + +void *git__reallocarray(void *ptr, size_t nelem, size_t elsize) +{ + size_t newsize; + + if (GIT_MULTIPLY_SIZET_OVERFLOW(&newsize, nelem, elsize)) + return NULL; + + return git__realloc(ptr, newsize); +} + +void *git__mallocarray(size_t nelem, size_t elsize) +{ + return git__reallocarray(NULL, nelem, elsize); +} + +char *git__strdup(const char *str) +{ + size_t len = strlen(str) + 1; + void *ptr = git__malloc(len); + + if (ptr) + memcpy(ptr, str, len); + + return ptr; +} + +char *git__strndup(const char *str, size_t n) +{ + size_t len = p_strnlen(str, n); + char *ptr = git__malloc(len + 1); + + if (ptr) { + memcpy(ptr, str, len); + ptr[len] = '\0'; + } + + return ptr; +} + +char *git__substrdup(const char *str, size_t n) +{ + char *ptr = git__malloc(n + 1); + + if (ptr) { + memcpy(ptr, str, n); + ptr[n] = '\0'; + } + + return ptr; +} + +static int setup_default_allocator(void) +{ +#if defined(GIT_WIN32_LEAKCHECK) + return git_win32_leakcheck_init_allocator(&git__allocator); +#else + return git_stdalloc_init_allocator(&git__allocator); +#endif +} + +int git_allocator_global_init(void) +{ + /* + * We don't want to overwrite any allocator which has been set + * before the init function is called. + */ + if (git__allocator.gmalloc != git_failalloc_malloc) + return 0; + + return setup_default_allocator(); +} + +int git_allocator_setup(git_allocator *allocator) +{ + if (!allocator) + return setup_default_allocator(); + + memcpy(&git__allocator, allocator, sizeof(*allocator)); + return 0; +} diff --git a/src/util/alloc.h b/src/util/alloc.h new file mode 100644 index 0000000..32b614b --- /dev/null +++ b/src/util/alloc.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_alloc_h__ +#define INCLUDE_alloc_h__ + +#include "git2/sys/alloc.h" + +#include "git2_util.h" + +extern git_allocator git__allocator; + +GIT_INLINE(void *) git__malloc(size_t len) +{ + void *p = git__allocator.gmalloc(len, __FILE__, __LINE__); + + if (!p) + git_error_set_oom(); + + return p; +} + +GIT_INLINE(void *) git__realloc(void *ptr, size_t size) +{ + void *p = git__allocator.grealloc(ptr, size, __FILE__, __LINE__); + + if (!p) + git_error_set_oom(); + + return p; +} + +GIT_INLINE(void) git__free(void *ptr) +{ + git__allocator.gfree(ptr); +} + +extern void *git__calloc(size_t nelem, size_t elsize); +extern void *git__mallocarray(size_t nelem, size_t elsize); +extern void *git__reallocarray(void *ptr, size_t nelem, size_t elsize); + +extern char *git__strdup(const char *str); +extern char *git__strndup(const char *str, size_t n); +extern char *git__substrdup(const char *str, size_t n); + +/** + * This function is being called by our global setup routines to + * initialize the standard allocator. + */ +int git_allocator_global_init(void); + +/** + * Switch out libgit2's global memory allocator + * + * @param allocator The new allocator that should be used. All function pointers + * of it need to be set correctly. + * @return An error code or 0. + */ +int git_allocator_setup(git_allocator *allocator); + +#endif diff --git a/src/util/allocators/failalloc.c b/src/util/allocators/failalloc.c new file mode 100644 index 0000000..c1025e3 --- /dev/null +++ b/src/util/allocators/failalloc.c @@ -0,0 +1,32 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "failalloc.h" + +void *git_failalloc_malloc(size_t len, const char *file, int line) +{ + GIT_UNUSED(len); + GIT_UNUSED(file); + GIT_UNUSED(line); + + return NULL; +} + +void *git_failalloc_realloc(void *ptr, size_t size, const char *file, int line) +{ + GIT_UNUSED(ptr); + GIT_UNUSED(size); + GIT_UNUSED(file); + GIT_UNUSED(line); + + return NULL; +} + +void git_failalloc_free(void *ptr) +{ + GIT_UNUSED(ptr); +} diff --git a/src/util/allocators/failalloc.h b/src/util/allocators/failalloc.h new file mode 100644 index 0000000..a3788e6 --- /dev/null +++ b/src/util/allocators/failalloc.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_allocators_failalloc_h__ +#define INCLUDE_allocators_failalloc_h__ + +#include "git2_util.h" + +extern void *git_failalloc_malloc(size_t len, const char *file, int line); +extern void *git_failalloc_realloc(void *ptr, size_t size, const char *file, int line); +extern void git_failalloc_free(void *ptr); + +#endif diff --git a/src/util/allocators/stdalloc.c b/src/util/allocators/stdalloc.c new file mode 100644 index 0000000..f2d72a7 --- /dev/null +++ b/src/util/allocators/stdalloc.c @@ -0,0 +1,47 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "stdalloc.h" + +static void *stdalloc__malloc(size_t len, const char *file, int line) +{ + GIT_UNUSED(file); + GIT_UNUSED(line); + +#ifdef GIT_DEBUG_STRICT_ALLOC + if (!len) + return NULL; +#endif + + return malloc(len); +} + +static void *stdalloc__realloc(void *ptr, size_t size, const char *file, int line) +{ + GIT_UNUSED(file); + GIT_UNUSED(line); + +#ifdef GIT_DEBUG_STRICT_ALLOC + if (!size) + return NULL; +#endif + + return realloc(ptr, size); +} + +static void stdalloc__free(void *ptr) +{ + free(ptr); +} + +int git_stdalloc_init_allocator(git_allocator *allocator) +{ + allocator->gmalloc = stdalloc__malloc; + allocator->grealloc = stdalloc__realloc; + allocator->gfree = stdalloc__free; + return 0; +} diff --git a/src/util/allocators/stdalloc.h b/src/util/allocators/stdalloc.h new file mode 100644 index 0000000..955038c --- /dev/null +++ b/src/util/allocators/stdalloc.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_allocators_stdalloc_h__ +#define INCLUDE_allocators_stdalloc_h__ + +#include "git2_util.h" + +#include "alloc.h" + +int git_stdalloc_init_allocator(git_allocator *allocator); + +#endif diff --git a/src/util/allocators/win32_leakcheck.c b/src/util/allocators/win32_leakcheck.c new file mode 100644 index 0000000..cdf16d3 --- /dev/null +++ b/src/util/allocators/win32_leakcheck.c @@ -0,0 +1,50 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "win32_leakcheck.h" + +#if defined(GIT_WIN32_LEAKCHECK) + +#include "win32/w32_leakcheck.h" + +static void *leakcheck_malloc(size_t len, const char *file, int line) +{ + void *ptr = _malloc_dbg(len, _NORMAL_BLOCK, git_win32_leakcheck_stacktrace(1,file), line); + if (!ptr) git_error_set_oom(); + return ptr; +} + +static void *leakcheck_realloc(void *ptr, size_t size, const char *file, int line) +{ + void *new_ptr = _realloc_dbg(ptr, size, _NORMAL_BLOCK, git_win32_leakcheck_stacktrace(1,file), line); + if (!new_ptr) git_error_set_oom(); + return new_ptr; +} + +static void leakcheck_free(void *ptr) +{ + free(ptr); +} + +int git_win32_leakcheck_init_allocator(git_allocator *allocator) +{ + allocator->gmalloc = leakcheck_malloc; + allocator->grealloc = leakcheck_realloc; + allocator->gfree = leakcheck_free; + return 0; +} + +#else + +int git_win32_leakcheck_init_allocator(git_allocator *allocator) +{ + GIT_UNUSED(allocator); + git_error_set(GIT_EINVALID, "leakcheck memory allocator not available"); + return -1; +} + +#endif diff --git a/src/util/allocators/win32_leakcheck.h b/src/util/allocators/win32_leakcheck.h new file mode 100644 index 0000000..edcd930 --- /dev/null +++ b/src/util/allocators/win32_leakcheck.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_allocators_win32_leakcheck_h +#define INCLUDE_allocators_win32_leakcheck_h + +#include "git2_util.h" + +#include "alloc.h" + +int git_win32_leakcheck_init_allocator(git_allocator *allocator); + +#endif diff --git a/src/util/array.h b/src/util/array.h new file mode 100644 index 0000000..633d598 --- /dev/null +++ b/src/util/array.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_array_h__ +#define INCLUDE_array_h__ + +#include "git2_util.h" + +/* + * Use this to declare a typesafe resizable array of items, a la: + * + * git_array_t(int) my_ints = GIT_ARRAY_INIT; + * ... + * int *i = git_array_alloc(my_ints); + * GIT_ERROR_CHECK_ALLOC(i); + * ... + * git_array_clear(my_ints); + * + * You may also want to do things like: + * + * typedef git_array_t(my_struct) my_struct_array_t; + */ +#define git_array_t(type) struct { type *ptr; size_t size, asize; } + +#define GIT_ARRAY_INIT { NULL, 0, 0 } + +#define git_array_init(a) \ + do { (a).size = (a).asize = 0; (a).ptr = NULL; } while (0) + +#define git_array_init_to_size(a, desired) \ + do { (a).size = 0; (a).asize = desired; (a).ptr = git__calloc(desired, sizeof(*(a).ptr)); } while (0) + +#define git_array_dispose(a) \ + do { git__free((a).ptr); } while (0) + +#define git_array_clear(a) \ + do { git__free((a).ptr); git_array_init(a); } while (0) + +#define GIT_ERROR_CHECK_ARRAY(a) GIT_ERROR_CHECK_ALLOC((a).ptr) + + +typedef git_array_t(char) git_array_generic_t; + +/* use a generic array for growth, return 0 on success */ +GIT_INLINE(int) git_array_grow(void *_a, size_t item_size) +{ + volatile git_array_generic_t *a = _a; + size_t new_size; + char *new_array; + + if (a->size < 8) { + new_size = 8; + } else { + if (GIT_MULTIPLY_SIZET_OVERFLOW(&new_size, a->size, 3)) + goto on_oom; + new_size /= 2; + } + + if ((new_array = git__reallocarray(a->ptr, new_size, item_size)) == NULL) + goto on_oom; + + a->ptr = new_array; + a->asize = new_size; + return 0; + +on_oom: + git_array_clear(*a); + return -1; +} + +#define git_array_alloc(a) \ + (((a).size < (a).asize || git_array_grow(&(a), sizeof(*(a).ptr)) == 0) ? \ + &(a).ptr[(a).size++] : (void *)NULL) + +#define git_array_last(a) ((a).size ? &(a).ptr[(a).size - 1] : (void *)NULL) + +#define git_array_pop(a) ((a).size ? &(a).ptr[--(a).size] : (void *)NULL) + +#define git_array_get(a, i) (((i) < (a).size) ? &(a).ptr[(i)] : (void *)NULL) + +#define git_array_size(a) (a).size + +#define git_array_valid_index(a, i) ((i) < (a).size) + +#define git_array_foreach(a, i, element) \ + for ((i) = 0; (i) < (a).size && ((element) = &(a).ptr[(i)]); (i)++) + +typedef int (*git_array_compare_cb)(const void *, const void *); + +GIT_INLINE(int) git_array__search( + size_t *out, + void *array_ptr, + size_t item_size, + size_t array_len, + git_array_compare_cb compare, + const void *key) +{ + size_t lim; + unsigned char *part, *array = array_ptr, *base = array_ptr; + int cmp = -1; + + for (lim = array_len; lim != 0; lim >>= 1) { + part = base + (lim >> 1) * item_size; + cmp = (*compare)(key, part); + + if (cmp == 0) { + base = part; + break; + } + if (cmp > 0) { /* key > p; take right partition */ + base = part + 1 * item_size; + lim--; + } /* else take left partition */ + } + + if (out) + *out = (base - array) / item_size; + + return (cmp == 0) ? 0 : GIT_ENOTFOUND; +} + +#define git_array_search(out, a, cmp, key) \ + git_array__search(out, (a).ptr, sizeof(*(a).ptr), (a).size, \ + (cmp), (key)) + +#endif diff --git a/src/util/assert_safe.h b/src/util/assert_safe.h new file mode 100644 index 0000000..cc0bac5 --- /dev/null +++ b/src/util/assert_safe.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_assert_safe_h__ +#define INCLUDE_assert_safe_h__ + +/* + * In a debug build, we'll assert(3) for aide in debugging. In release + * builds, we will provide macros that will set an error message that + * indicate a failure and return. Note that memory leaks can occur in + * a release-mode assertion failure -- it is impractical to provide + * safe clean up routines in these very extreme failures, but care + * should be taken to not leak very large objects. + */ + +#if (defined(_DEBUG) || defined(GIT_ASSERT_HARD)) && GIT_ASSERT_HARD != 0 +# include + +# define GIT_ASSERT(expr) assert(expr) +# define GIT_ASSERT_ARG(expr) assert(expr) + +# define GIT_ASSERT_WITH_RETVAL(expr, fail) assert(expr) +# define GIT_ASSERT_ARG_WITH_RETVAL(expr, fail) assert(expr) + +# define GIT_ASSERT_WITH_CLEANUP(expr, cleanup) assert(expr) +#else + +/** Internal consistency check to stop the function. */ +# define GIT_ASSERT(expr) GIT_ASSERT_WITH_RETVAL(expr, -1) + +/** + * Assert that a consumer-provided argument is valid, setting an + * actionable error message and returning -1 if it is not. + */ +# define GIT_ASSERT_ARG(expr) GIT_ASSERT_ARG_WITH_RETVAL(expr, -1) + +/** Internal consistency check to return the `fail` param on failure. */ +# define GIT_ASSERT_WITH_RETVAL(expr, fail) \ + GIT_ASSERT__WITH_RETVAL(expr, GIT_ERROR_INTERNAL, "unrecoverable internal error", fail) + +/** + * Assert that a consumer-provided argument is valid, setting an + * actionable error message and returning the `fail` param if not. + */ +# define GIT_ASSERT_ARG_WITH_RETVAL(expr, fail) \ + GIT_ASSERT__WITH_RETVAL(expr, GIT_ERROR_INVALID, "invalid argument", fail) + +# define GIT_ASSERT__WITH_RETVAL(expr, code, msg, fail) do { \ + if (!(expr)) { \ + git_error_set(code, "%s: '%s'", msg, #expr); \ + return fail; \ + } \ + } while(0) + +/** + * Go to to the given label on assertion failures; useful when you have + * taken a lock or otherwise need to release a resource. + */ +# define GIT_ASSERT_WITH_CLEANUP(expr, cleanup) \ + GIT_ASSERT__WITH_CLEANUP(expr, GIT_ERROR_INTERNAL, "unrecoverable internal error", cleanup) + +# define GIT_ASSERT__WITH_CLEANUP(expr, code, msg, cleanup) do { \ + if (!(expr)) { \ + git_error_set(code, "%s: '%s'", msg, #expr); \ + cleanup; \ + } \ + } while(0) + +#endif /* GIT_ASSERT_HARD */ + +#endif diff --git a/src/util/bitvec.h b/src/util/bitvec.h new file mode 100644 index 0000000..544832d --- /dev/null +++ b/src/util/bitvec.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_bitvec_h__ +#define INCLUDE_bitvec_h__ + +#include "common.h" + +/* + * This is a silly little fixed length bit vector type that will store + * vectors of 64 bits or less directly in the structure and allocate + * memory for vectors longer than 64 bits. You can use the two versions + * transparently through the API and avoid heap allocation completely when + * using a short bit vector as a result. + */ +typedef struct { + size_t length; + union { + uint64_t *words; + uint64_t bits; + } u; +} git_bitvec; + +GIT_INLINE(int) git_bitvec_init(git_bitvec *bv, size_t capacity) +{ + memset(bv, 0x0, sizeof(*bv)); + + if (capacity >= 64) { + bv->length = (capacity / 64) + 1; + bv->u.words = git__calloc(bv->length, sizeof(uint64_t)); + if (!bv->u.words) + return -1; + } + + return 0; +} + +#define GIT_BITVEC_MASK(BIT) ((uint64_t)1 << (BIT % 64)) +#define GIT_BITVEC_WORD(BV, BIT) (BV->length ? &BV->u.words[BIT / 64] : &BV->u.bits) + +GIT_INLINE(void) git_bitvec_set(git_bitvec *bv, size_t bit, bool on) +{ + uint64_t *word = GIT_BITVEC_WORD(bv, bit); + uint64_t mask = GIT_BITVEC_MASK(bit); + + if (on) + *word |= mask; + else + *word &= ~mask; +} + +GIT_INLINE(bool) git_bitvec_get(git_bitvec *bv, size_t bit) +{ + uint64_t *word = GIT_BITVEC_WORD(bv, bit); + return (*word & GIT_BITVEC_MASK(bit)) != 0; +} + +GIT_INLINE(void) git_bitvec_clear(git_bitvec *bv) +{ + if (!bv->length) + bv->u.bits = 0; + else + memset(bv->u.words, 0x0, bv->length * sizeof(uint64_t)); +} + +GIT_INLINE(void) git_bitvec_free(git_bitvec *bv) +{ + if (bv->length) + git__free(bv->u.words); +} + +#endif diff --git a/src/util/cc-compat.h b/src/util/cc-compat.h new file mode 100644 index 0000000..ede6e9a --- /dev/null +++ b/src/util/cc-compat.h @@ -0,0 +1,108 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_cc_compat_h__ +#define INCLUDE_cc_compat_h__ + +#include + +/* + * See if our compiler is known to support flexible array members. + */ +#ifndef GIT_FLEX_ARRAY +# if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) +# define GIT_FLEX_ARRAY /* empty */ +# elif defined(__GNUC__) +# if (__GNUC__ >= 3) +# define GIT_FLEX_ARRAY /* empty */ +# else +# define GIT_FLEX_ARRAY 0 /* older GNU extension */ +# endif +# endif + +/* Default to safer but a bit wasteful traditional style */ +# ifndef GIT_FLEX_ARRAY +# define GIT_FLEX_ARRAY 1 +# endif +#endif + +#if defined(__GNUC__) +# define GIT_ALIGN(x,size) x __attribute__ ((aligned(size))) +#elif defined(_MSC_VER) +# define GIT_ALIGN(x,size) __declspec(align(size)) x +#else +# define GIT_ALIGN(x,size) x +#endif + +#if defined(__GNUC__) +# define GIT_UNUSED(x) \ + do { \ + __typeof__(x) _unused __attribute__((unused)); \ + _unused = (x); \ + } while (0) +# define GIT_UNUSED_ARG __attribute__((unused)) +#else +# define GIT_UNUSED(x) ((void)(x)) +# define GIT_UNUSED_ARG +#endif + +/* Define the printf format specifier to use for size_t output */ +#if defined(_MSC_VER) || defined(__MINGW32__) + +/* Visual Studio 2012 and prior lack PRId64 entirely */ +# ifndef PRId64 +# define PRId64 "I64d" +# endif + +/* The first block is needed to avoid warnings on MingW amd64 */ +# if (SIZE_MAX == ULLONG_MAX) +# define PRIuZ "I64u" +# define PRIxZ "I64x" +# define PRIXZ "I64X" +# define PRIdZ "I64d" +# else +# define PRIuZ "Iu" +# define PRIxZ "Ix" +# define PRIXZ "IX" +# define PRIdZ "Id" +# endif + +#else +# define PRIuZ "zu" +# define PRIxZ "zx" +# define PRIXZ "zX" +# define PRIdZ "zd" +#endif + +/* Microsoft Visual C/C++ */ +#if defined(_MSC_VER) +/* disable "deprecated function" warnings */ +# pragma warning ( disable : 4996 ) +/* disable "conditional expression is constant" level 4 warnings */ +# pragma warning ( disable : 4127 ) +#endif + +#if defined (_MSC_VER) + typedef unsigned char bool; +# ifndef true +# define true 1 +# endif +# ifndef false +# define false 0 +# endif +#else +# include +#endif + +#ifndef va_copy +# ifdef __va_copy +# define va_copy(dst, src) __va_copy(dst, src) +# else +# define va_copy(dst, src) ((dst) = (src)) +# endif +#endif + +#endif diff --git a/src/util/date.c b/src/util/date.c new file mode 100644 index 0000000..4d757e2 --- /dev/null +++ b/src/util/date.c @@ -0,0 +1,899 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2_util.h" + +#ifndef GIT_WIN32 +#include +#endif + +#include "util.h" +#include "posix.h" +#include "date.h" + +#include +#include + +typedef enum { + DATE_NORMAL = 0, + DATE_RELATIVE, + DATE_SHORT, + DATE_LOCAL, + DATE_ISO8601, + DATE_RFC2822, + DATE_RAW +} date_mode; + +/* + * This is like mktime, but without normalization of tm_wday and tm_yday. + */ +static git_time_t tm_to_time_t(const struct tm *tm) +{ + static const int mdays[] = { + 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 + }; + int year = tm->tm_year - 70; + int month = tm->tm_mon; + int day = tm->tm_mday; + + if (year < 0 || year > 129) /* algo only works for 1970-2099 */ + return -1; + if (month < 0 || month > 11) /* array bounds */ + return -1; + if (month < 2 || (year + 2) % 4) + day--; + if (tm->tm_hour < 0 || tm->tm_min < 0 || tm->tm_sec < 0) + return -1; + return (year * 365 + (year + 1) / 4 + mdays[month] + day) * 24*60*60UL + + tm->tm_hour * 60*60 + tm->tm_min * 60 + tm->tm_sec; +} + +static const char *month_names[] = { + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" +}; + +static const char *weekday_names[] = { + "Sundays", "Mondays", "Tuesdays", "Wednesdays", "Thursdays", "Fridays", "Saturdays" +}; + + + +/* + * Check these. And note how it doesn't do the summer-time conversion. + * + * In my world, it's always summer, and things are probably a bit off + * in other ways too. + */ +static const struct { + const char *name; + int offset; + int dst; +} timezone_names[] = { + { "IDLW", -12, 0, }, /* International Date Line West */ + { "NT", -11, 0, }, /* Nome */ + { "CAT", -10, 0, }, /* Central Alaska */ + { "HST", -10, 0, }, /* Hawaii Standard */ + { "HDT", -10, 1, }, /* Hawaii Daylight */ + { "YST", -9, 0, }, /* Yukon Standard */ + { "YDT", -9, 1, }, /* Yukon Daylight */ + { "PST", -8, 0, }, /* Pacific Standard */ + { "PDT", -8, 1, }, /* Pacific Daylight */ + { "MST", -7, 0, }, /* Mountain Standard */ + { "MDT", -7, 1, }, /* Mountain Daylight */ + { "CST", -6, 0, }, /* Central Standard */ + { "CDT", -6, 1, }, /* Central Daylight */ + { "EST", -5, 0, }, /* Eastern Standard */ + { "EDT", -5, 1, }, /* Eastern Daylight */ + { "AST", -3, 0, }, /* Atlantic Standard */ + { "ADT", -3, 1, }, /* Atlantic Daylight */ + { "WAT", -1, 0, }, /* West Africa */ + + { "GMT", 0, 0, }, /* Greenwich Mean */ + { "UTC", 0, 0, }, /* Universal (Coordinated) */ + { "Z", 0, 0, }, /* Zulu, alias for UTC */ + + { "WET", 0, 0, }, /* Western European */ + { "BST", 0, 1, }, /* British Summer */ + { "CET", +1, 0, }, /* Central European */ + { "MET", +1, 0, }, /* Middle European */ + { "MEWT", +1, 0, }, /* Middle European Winter */ + { "MEST", +1, 1, }, /* Middle European Summer */ + { "CEST", +1, 1, }, /* Central European Summer */ + { "MESZ", +1, 1, }, /* Middle European Summer */ + { "FWT", +1, 0, }, /* French Winter */ + { "FST", +1, 1, }, /* French Summer */ + { "EET", +2, 0, }, /* Eastern Europe */ + { "EEST", +2, 1, }, /* Eastern European Daylight */ + { "WAST", +7, 0, }, /* West Australian Standard */ + { "WADT", +7, 1, }, /* West Australian Daylight */ + { "CCT", +8, 0, }, /* China Coast */ + { "JST", +9, 0, }, /* Japan Standard */ + { "EAST", +10, 0, }, /* Eastern Australian Standard */ + { "EADT", +10, 1, }, /* Eastern Australian Daylight */ + { "GST", +10, 0, }, /* Guam Standard */ + { "NZT", +12, 0, }, /* New Zealand */ + { "NZST", +12, 0, }, /* New Zealand Standard */ + { "NZDT", +12, 1, }, /* New Zealand Daylight */ + { "IDLE", +12, 0, }, /* International Date Line East */ +}; + +static size_t match_string(const char *date, const char *str) +{ + size_t i = 0; + + for (i = 0; *date; date++, str++, i++) { + if (*date == *str) + continue; + if (toupper(*date) == toupper(*str)) + continue; + if (!isalnum(*date)) + break; + return 0; + } + return i; +} + +static int skip_alpha(const char *date) +{ + int i = 0; + do { + i++; + } while (isalpha(date[i])); + return i; +} + +/* +* Parse month, weekday, or timezone name +*/ +static size_t match_alpha(const char *date, struct tm *tm, int *offset) +{ + unsigned int i; + + for (i = 0; i < 12; i++) { + size_t match = match_string(date, month_names[i]); + if (match >= 3) { + tm->tm_mon = i; + return match; + } + } + + for (i = 0; i < 7; i++) { + size_t match = match_string(date, weekday_names[i]); + if (match >= 3) { + tm->tm_wday = i; + return match; + } + } + + for (i = 0; i < ARRAY_SIZE(timezone_names); i++) { + size_t match = match_string(date, timezone_names[i].name); + if (match >= 3 || match == strlen(timezone_names[i].name)) { + int off = timezone_names[i].offset; + + /* This is bogus, but we like summer */ + off += timezone_names[i].dst; + + /* Only use the tz name offset if we don't have anything better */ + if (*offset == -1) + *offset = 60*off; + + return match; + } + } + + if (match_string(date, "PM") == 2) { + tm->tm_hour = (tm->tm_hour % 12) + 12; + return 2; + } + + if (match_string(date, "AM") == 2) { + tm->tm_hour = (tm->tm_hour % 12) + 0; + return 2; + } + + /* BAD */ + return skip_alpha(date); +} + +static int is_date(int year, int month, int day, struct tm *now_tm, time_t now, struct tm *tm) +{ + if (month > 0 && month < 13 && day > 0 && day < 32) { + struct tm check = *tm; + struct tm *r = (now_tm ? &check : tm); + git_time_t specified; + + r->tm_mon = month - 1; + r->tm_mday = day; + if (year == -1) { + if (!now_tm) + return 1; + r->tm_year = now_tm->tm_year; + } + else if (year >= 1970 && year < 2100) + r->tm_year = year - 1900; + else if (year > 70 && year < 100) + r->tm_year = year; + else if (year < 38) + r->tm_year = year + 100; + else + return 0; + if (!now_tm) + return 1; + + specified = tm_to_time_t(r); + + /* Be it commit time or author time, it does not make + * sense to specify timestamp way into the future. Make + * sure it is not later than ten days from now... + */ + if (now + 10*24*3600 < specified) + return 0; + tm->tm_mon = r->tm_mon; + tm->tm_mday = r->tm_mday; + if (year != -1) + tm->tm_year = r->tm_year; + return 1; + } + return 0; +} + +static size_t match_multi_number(unsigned long num, char c, const char *date, char *end, struct tm *tm) +{ + time_t now; + struct tm now_tm; + struct tm *refuse_future; + long num2, num3; + + num2 = strtol(end+1, &end, 10); + num3 = -1; + if (*end == c && isdigit(end[1])) + num3 = strtol(end+1, &end, 10); + + /* Time? Date? */ + switch (c) { + case ':': + if (num3 < 0) + num3 = 0; + if (num < 25 && num2 >= 0 && num2 < 60 && num3 >= 0 && num3 <= 60) { + tm->tm_hour = num; + tm->tm_min = num2; + tm->tm_sec = num3; + break; + } + return 0; + + case '-': + case '/': + case '.': + now = time(NULL); + refuse_future = NULL; + if (p_gmtime_r(&now, &now_tm)) + refuse_future = &now_tm; + + if (num > 70) { + /* yyyy-mm-dd? */ + if (is_date(num, num2, num3, refuse_future, now, tm)) + break; + /* yyyy-dd-mm? */ + if (is_date(num, num3, num2, refuse_future, now, tm)) + break; + } + /* Our eastern European friends say dd.mm.yy[yy] + * is the norm there, so giving precedence to + * mm/dd/yy[yy] form only when separator is not '.' + */ + if (c != '.' && + is_date(num3, num, num2, refuse_future, now, tm)) + break; + /* European dd.mm.yy[yy] or funny US dd/mm/yy[yy] */ + if (is_date(num3, num2, num, refuse_future, now, tm)) + break; + /* Funny European mm.dd.yy */ + if (c == '.' && + is_date(num3, num, num2, refuse_future, now, tm)) + break; + return 0; + } + return end - date; +} + +/* + * Have we filled in any part of the time/date yet? + * We just do a binary 'and' to see if the sign bit + * is set in all the values. + */ +static int nodate(struct tm *tm) +{ + return (tm->tm_year & + tm->tm_mon & + tm->tm_mday & + tm->tm_hour & + tm->tm_min & + tm->tm_sec) < 0; +} + +/* + * We've seen a digit. Time? Year? Date? + */ +static size_t match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt) +{ + size_t n; + char *end; + unsigned long num; + + num = strtoul(date, &end, 10); + + /* + * Seconds since 1970? We trigger on that for any numbers with + * more than 8 digits. This is because we don't want to rule out + * numbers like 20070606 as a YYYYMMDD date. + */ + if (num >= 100000000 && nodate(tm)) { + time_t time = num; + if (p_gmtime_r(&time, tm)) { + *tm_gmt = 1; + return end - date; + } + } + + /* + * Check for special formats: num[-.:/]num[same]num + */ + switch (*end) { + case ':': + case '.': + case '/': + case '-': + if (isdigit(end[1])) { + size_t match = match_multi_number(num, *end, date, end, tm); + if (match) + return match; + } + } + + /* + * None of the special formats? Try to guess what + * the number meant. We use the number of digits + * to make a more educated guess.. + */ + n = 0; + do { + n++; + } while (isdigit(date[n])); + + /* Four-digit year or a timezone? */ + if (n == 4) { + if (num <= 1400 && *offset == -1) { + unsigned int minutes = num % 100; + unsigned int hours = num / 100; + *offset = hours*60 + minutes; + } else if (num > 1900 && num < 2100) + tm->tm_year = num - 1900; + return n; + } + + /* + * Ignore lots of numerals. We took care of 4-digit years above. + * Days or months must be one or two digits. + */ + if (n > 2) + return n; + + /* + * NOTE! We will give precedence to day-of-month over month or + * year numbers in the 1-12 range. So 05 is always "mday 5", + * unless we already have a mday.. + * + * IOW, 01 Apr 05 parses as "April 1st, 2005". + */ + if (num > 0 && num < 32 && tm->tm_mday < 0) { + tm->tm_mday = num; + return n; + } + + /* Two-digit year? */ + if (n == 2 && tm->tm_year < 0) { + if (num < 10 && tm->tm_mday >= 0) { + tm->tm_year = num + 100; + return n; + } + if (num >= 70) { + tm->tm_year = num; + return n; + } + } + + if (num > 0 && num < 13 && tm->tm_mon < 0) + tm->tm_mon = num-1; + + return n; +} + +static size_t match_tz(const char *date, int *offp) +{ + char *end; + int hour = strtoul(date + 1, &end, 10); + size_t n = end - (date + 1); + int min = 0; + + if (n == 4) { + /* hhmm */ + min = hour % 100; + hour = hour / 100; + } else if (n != 2) { + min = 99; /* random stuff */ + } else if (*end == ':') { + /* hh:mm? */ + min = strtoul(end + 1, &end, 10); + if (end - (date + 1) != 5) + min = 99; /* random stuff */ + } /* otherwise we parsed "hh" */ + + /* + * Don't accept any random stuff. Even though some places have + * offset larger than 12 hours (e.g. Pacific/Kiritimati is at + * UTC+14), there is something wrong if hour part is much + * larger than that. We might also want to check that the + * minutes are divisible by 15 or something too. (Offset of + * Kathmandu, Nepal is UTC+5:45) + */ + if (min < 60 && hour < 24) { + int offset = hour * 60 + min; + if (*date == '-') + offset = -offset; + *offp = offset; + } + return end - date; +} + +/* + * Parse a string like "0 +0000" as ancient timestamp near epoch, but + * only when it appears not as part of any other string. + */ +static int match_object_header_date(const char *date, git_time_t *timestamp, int *offset) +{ + char *end; + unsigned long stamp; + int ofs; + + if (*date < '0' || '9' <= *date) + return -1; + stamp = strtoul(date, &end, 10); + if (*end != ' ' || stamp == ULONG_MAX || (end[1] != '+' && end[1] != '-')) + return -1; + date = end + 2; + ofs = strtol(date, &end, 10); + if ((*end != '\0' && (*end != '\n')) || end != date + 4) + return -1; + ofs = (ofs / 100) * 60 + (ofs % 100); + if (date[-1] == '-') + ofs = -ofs; + *timestamp = stamp; + *offset = ofs; + return 0; +} + +/* Gr. strptime is crap for this; it doesn't have a way to require RFC2822 + (i.e. English) day/month names, and it doesn't work correctly with %z. */ +static int parse_date_basic(const char *date, git_time_t *timestamp, int *offset) +{ + struct tm tm; + int tm_gmt; + git_time_t dummy_timestamp; + int dummy_offset; + + if (!timestamp) + timestamp = &dummy_timestamp; + if (!offset) + offset = &dummy_offset; + + memset(&tm, 0, sizeof(tm)); + tm.tm_year = -1; + tm.tm_mon = -1; + tm.tm_mday = -1; + tm.tm_isdst = -1; + tm.tm_hour = -1; + tm.tm_min = -1; + tm.tm_sec = -1; + *offset = -1; + tm_gmt = 0; + + if (*date == '@' && + !match_object_header_date(date + 1, timestamp, offset)) + return 0; /* success */ + for (;;) { + size_t match = 0; + unsigned char c = *date; + + /* Stop at end of string or newline */ + if (!c || c == '\n') + break; + + if (isalpha(c)) + match = match_alpha(date, &tm, offset); + else if (isdigit(c)) + match = match_digit(date, &tm, offset, &tm_gmt); + else if ((c == '-' || c == '+') && isdigit(date[1])) + match = match_tz(date, offset); + + if (!match) { + /* BAD */ + match = 1; + } + + date += match; + } + + /* mktime uses local timezone */ + *timestamp = tm_to_time_t(&tm); + if (*offset == -1) + *offset = (int)((time_t)*timestamp - mktime(&tm)) / 60; + + if (*timestamp == (git_time_t)-1) + return -1; + + if (!tm_gmt) + *timestamp -= *offset * 60; + return 0; /* success */ +} + + +/* + * Relative time update (eg "2 days ago"). If we haven't set the time + * yet, we need to set it from current time. + */ +static git_time_t update_tm(struct tm *tm, struct tm *now, unsigned long sec) +{ + time_t n; + + if (tm->tm_mday < 0) + tm->tm_mday = now->tm_mday; + if (tm->tm_mon < 0) + tm->tm_mon = now->tm_mon; + if (tm->tm_year < 0) { + tm->tm_year = now->tm_year; + if (tm->tm_mon > now->tm_mon) + tm->tm_year--; + } + + n = mktime(tm) - sec; + p_localtime_r(&n, tm); + return n; +} + +static void date_now(struct tm *tm, struct tm *now, int *num) +{ + GIT_UNUSED(num); + update_tm(tm, now, 0); +} + +static void date_yesterday(struct tm *tm, struct tm *now, int *num) +{ + GIT_UNUSED(num); + update_tm(tm, now, 24*60*60); +} + +static void date_time(struct tm *tm, struct tm *now, int hour) +{ + if (tm->tm_hour < hour) + date_yesterday(tm, now, NULL); + tm->tm_hour = hour; + tm->tm_min = 0; + tm->tm_sec = 0; +} + +static void date_midnight(struct tm *tm, struct tm *now, int *num) +{ + GIT_UNUSED(num); + date_time(tm, now, 0); +} + +static void date_noon(struct tm *tm, struct tm *now, int *num) +{ + GIT_UNUSED(num); + date_time(tm, now, 12); +} + +static void date_tea(struct tm *tm, struct tm *now, int *num) +{ + GIT_UNUSED(num); + date_time(tm, now, 17); +} + +static void date_pm(struct tm *tm, struct tm *now, int *num) +{ + int hour, n = *num; + *num = 0; + GIT_UNUSED(now); + + hour = tm->tm_hour; + if (n) { + hour = n; + tm->tm_min = 0; + tm->tm_sec = 0; + } + tm->tm_hour = (hour % 12) + 12; +} + +static void date_am(struct tm *tm, struct tm *now, int *num) +{ + int hour, n = *num; + *num = 0; + GIT_UNUSED(now); + + hour = tm->tm_hour; + if (n) { + hour = n; + tm->tm_min = 0; + tm->tm_sec = 0; + } + tm->tm_hour = (hour % 12); +} + +static void date_never(struct tm *tm, struct tm *now, int *num) +{ + time_t n = 0; + GIT_UNUSED(now); + GIT_UNUSED(num); + p_localtime_r(&n, tm); +} + +static const struct special { + const char *name; + void (*fn)(struct tm *, struct tm *, int *); +} special[] = { + { "yesterday", date_yesterday }, + { "noon", date_noon }, + { "midnight", date_midnight }, + { "tea", date_tea }, + { "PM", date_pm }, + { "AM", date_am }, + { "never", date_never }, + { "now", date_now }, + { NULL } +}; + +static const char *number_name[] = { + "zero", "one", "two", "three", "four", + "five", "six", "seven", "eight", "nine", "ten", +}; + +static const struct typelen { + const char *type; + int length; +} typelen[] = { + { "seconds", 1 }, + { "minutes", 60 }, + { "hours", 60*60 }, + { "days", 24*60*60 }, + { "weeks", 7*24*60*60 }, + { NULL } +}; + +static const char *approxidate_alpha(const char *date, struct tm *tm, struct tm *now, int *num, int *touched) +{ + const struct typelen *tl; + const struct special *s; + const char *end = date; + int i; + + while (isalpha(*++end)) + /* scan to non-alpha */; + + for (i = 0; i < 12; i++) { + size_t match = match_string(date, month_names[i]); + if (match >= 3) { + tm->tm_mon = i; + *touched = 1; + return end; + } + } + + for (s = special; s->name; s++) { + size_t len = strlen(s->name); + if (match_string(date, s->name) == len) { + s->fn(tm, now, num); + *touched = 1; + return end; + } + } + + if (!*num) { + for (i = 1; i < 11; i++) { + size_t len = strlen(number_name[i]); + if (match_string(date, number_name[i]) == len) { + *num = i; + *touched = 1; + return end; + } + } + if (match_string(date, "last") == 4) { + *num = 1; + *touched = 1; + } + return end; + } + + tl = typelen; + while (tl->type) { + size_t len = strlen(tl->type); + if (match_string(date, tl->type) >= len-1) { + update_tm(tm, now, tl->length * (unsigned long)*num); + *num = 0; + *touched = 1; + return end; + } + tl++; + } + + for (i = 0; i < 7; i++) { + size_t match = match_string(date, weekday_names[i]); + if (match >= 3) { + int diff, n = *num -1; + *num = 0; + + diff = tm->tm_wday - i; + if (diff <= 0) + n++; + diff += 7*n; + + update_tm(tm, now, diff * 24 * 60 * 60); + *touched = 1; + return end; + } + } + + if (match_string(date, "months") >= 5) { + int n; + update_tm(tm, now, 0); /* fill in date fields if needed */ + n = tm->tm_mon - *num; + *num = 0; + while (n < 0) { + n += 12; + tm->tm_year--; + } + tm->tm_mon = n; + *touched = 1; + return end; + } + + if (match_string(date, "years") >= 4) { + update_tm(tm, now, 0); /* fill in date fields if needed */ + tm->tm_year -= *num; + *num = 0; + *touched = 1; + return end; + } + + return end; +} + +static const char *approxidate_digit(const char *date, struct tm *tm, int *num) +{ + char *end; + unsigned long number = strtoul(date, &end, 10); + + switch (*end) { + case ':': + case '.': + case '/': + case '-': + if (isdigit(end[1])) { + size_t match = match_multi_number(number, *end, date, end, tm); + if (match) + return date + match; + } + } + + /* Accept zero-padding only for small numbers ("Dec 02", never "Dec 0002") */ + if (date[0] != '0' || end - date <= 2) + *num = number; + return end; +} + +/* + * Do we have a pending number at the end, or when + * we see a new one? Let's assume it's a month day, + * as in "Dec 6, 1992" + */ +static void pending_number(struct tm *tm, int *num) +{ + int number = *num; + + if (number) { + *num = 0; + if (tm->tm_mday < 0 && number < 32) + tm->tm_mday = number; + else if (tm->tm_mon < 0 && number < 13) + tm->tm_mon = number-1; + else if (tm->tm_year < 0) { + if (number > 1969 && number < 2100) + tm->tm_year = number - 1900; + else if (number > 69 && number < 100) + tm->tm_year = number; + else if (number < 38) + tm->tm_year = 100 + number; + /* We mess up for number = 00 ? */ + } + } +} + +static git_time_t approxidate_str(const char *date, + time_t time_sec, + int *error_ret) +{ + int number = 0; + int touched = 0; + struct tm tm = {0}, now; + + p_localtime_r(&time_sec, &tm); + now = tm; + + tm.tm_year = -1; + tm.tm_mon = -1; + tm.tm_mday = -1; + + for (;;) { + unsigned char c = *date; + if (!c) + break; + date++; + if (isdigit(c)) { + pending_number(&tm, &number); + date = approxidate_digit(date-1, &tm, &number); + touched = 1; + continue; + } + if (isalpha(c)) + date = approxidate_alpha(date-1, &tm, &now, &number, &touched); + } + pending_number(&tm, &number); + if (!touched) + *error_ret = -1; + return update_tm(&tm, &now, 0); +} + +int git_date_parse(git_time_t *out, const char *date) +{ + time_t time_sec; + git_time_t timestamp; + int offset, error_ret=0; + + if (!parse_date_basic(date, ×tamp, &offset)) { + *out = timestamp; + return 0; + } + + if (time(&time_sec) == -1) + return -1; + + *out = approxidate_str(date, time_sec, &error_ret); + return error_ret; +} + +int git_date_rfc2822_fmt(git_str *out, git_time_t time, int offset) +{ + time_t t; + struct tm gmt; + + GIT_ASSERT_ARG(out); + + t = (time_t) (time + offset * 60); + + if (p_gmtime_r(&t, &gmt) == NULL) + return -1; + + return git_str_printf(out, "%.3s, %u %.3s %.4u %02u:%02u:%02u %+03d%02d", + weekday_names[gmt.tm_wday], + gmt.tm_mday, + month_names[gmt.tm_mon], + gmt.tm_year + 1900, + gmt.tm_hour, gmt.tm_min, gmt.tm_sec, + offset / 60, offset % 60); +} + diff --git a/src/util/date.h b/src/util/date.h new file mode 100644 index 0000000..7ebd3c3 --- /dev/null +++ b/src/util/date.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_date_h__ +#define INCLUDE_date_h__ + +#include "util.h" +#include "str.h" + +/* + * Parse a string into a value as a git_time_t. + * + * Sample valid input: + * - "yesterday" + * - "July 17, 2003" + * - "2003-7-17 08:23" + */ +extern int git_date_parse(git_time_t *out, const char *date); + +/* + * Format a git_time as a RFC2822 string + * + * @param out buffer to store formatted date + * @param time the time to be formatted + * @param offset the timezone offset + * @return 0 if successful; -1 on error + */ +extern int git_date_rfc2822_fmt(git_str *out, git_time_t time, int offset); + +#endif diff --git a/src/util/filebuf.c b/src/util/filebuf.c new file mode 100644 index 0000000..7afb76b --- /dev/null +++ b/src/util/filebuf.c @@ -0,0 +1,600 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "filebuf.h" + +#include "futils.h" + +static const size_t WRITE_BUFFER_SIZE = (4096 * 2); + +enum buferr_t { + BUFERR_OK = 0, + BUFERR_WRITE, + BUFERR_ZLIB, + BUFERR_MEM +}; + +#define ENSURE_BUF_OK(buf) if ((buf)->last_error != BUFERR_OK) { return -1; } + +static int verify_last_error(git_filebuf *file) +{ + switch (file->last_error) { + case BUFERR_WRITE: + git_error_set(GIT_ERROR_OS, "failed to write out file"); + return -1; + + case BUFERR_MEM: + git_error_set_oom(); + return -1; + + case BUFERR_ZLIB: + git_error_set(GIT_ERROR_ZLIB, + "Buffer error when writing out ZLib data"); + return -1; + + default: + return 0; + } +} + +static int lock_file(git_filebuf *file, int flags, mode_t mode) +{ + if (git_fs_path_exists(file->path_lock) == true) { + git_error_clear(); /* actual OS error code just confuses */ + git_error_set(GIT_ERROR_OS, + "failed to lock file '%s' for writing", file->path_lock); + return GIT_ELOCKED; + } + + /* create path to the file buffer is required */ + if (flags & GIT_FILEBUF_CREATE_LEADING_DIRS) { + /* XXX: Should dirmode here be configurable? Or is 0777 always fine? */ + file->fd = git_futils_creat_locked_withpath(file->path_lock, 0777, mode); + } else { + file->fd = git_futils_creat_locked(file->path_lock, mode); + } + + if (file->fd < 0) + return file->fd; + + file->fd_is_open = true; + + if ((flags & GIT_FILEBUF_APPEND) && git_fs_path_exists(file->path_original) == true) { + git_file source; + char buffer[GIT_BUFSIZE_FILEIO]; + ssize_t read_bytes; + int error = 0; + + source = p_open(file->path_original, O_RDONLY); + if (source < 0) { + git_error_set(GIT_ERROR_OS, + "failed to open file '%s' for reading", + file->path_original); + return -1; + } + + while ((read_bytes = p_read(source, buffer, sizeof(buffer))) > 0) { + if ((error = p_write(file->fd, buffer, read_bytes)) < 0) + break; + if (file->compute_digest) + git_hash_update(&file->digest, buffer, read_bytes); + } + + p_close(source); + + if (read_bytes < 0) { + git_error_set(GIT_ERROR_OS, "failed to read file '%s'", file->path_original); + return -1; + } else if (error < 0) { + git_error_set(GIT_ERROR_OS, "failed to write file '%s'", file->path_lock); + return -1; + } + } + + return 0; +} + +void git_filebuf_cleanup(git_filebuf *file) +{ + if (file->fd_is_open && file->fd >= 0) + p_close(file->fd); + + if (file->created_lock && !file->did_rename && file->path_lock && git_fs_path_exists(file->path_lock)) + p_unlink(file->path_lock); + + if (file->compute_digest) { + git_hash_ctx_cleanup(&file->digest); + file->compute_digest = 0; + } + + if (file->buffer) + git__free(file->buffer); + + /* use the presence of z_buf to decide if we need to deflateEnd */ + if (file->z_buf) { + git__free(file->z_buf); + deflateEnd(&file->zs); + } + + if (file->path_original) + git__free(file->path_original); + if (file->path_lock) + git__free(file->path_lock); + + memset(file, 0x0, sizeof(git_filebuf)); + file->fd = -1; +} + +GIT_INLINE(int) flush_buffer(git_filebuf *file) +{ + int result = file->write(file, file->buffer, file->buf_pos); + file->buf_pos = 0; + return result; +} + +int git_filebuf_flush(git_filebuf *file) +{ + return flush_buffer(file); +} + +static int write_normal(git_filebuf *file, void *source, size_t len) +{ + if (len > 0) { + if (p_write(file->fd, (void *)source, len) < 0) { + file->last_error = BUFERR_WRITE; + return -1; + } + + if (file->compute_digest) + git_hash_update(&file->digest, source, len); + } + + return 0; +} + +static int write_deflate(git_filebuf *file, void *source, size_t len) +{ + z_stream *zs = &file->zs; + + if (len > 0 || file->flush_mode == Z_FINISH) { + zs->next_in = source; + zs->avail_in = (uInt)len; + + do { + size_t have; + + zs->next_out = file->z_buf; + zs->avail_out = (uInt)file->buf_size; + + if (deflate(zs, file->flush_mode) == Z_STREAM_ERROR) { + file->last_error = BUFERR_ZLIB; + return -1; + } + + have = file->buf_size - (size_t)zs->avail_out; + + if (p_write(file->fd, file->z_buf, have) < 0) { + file->last_error = BUFERR_WRITE; + return -1; + } + + } while (zs->avail_out == 0); + + GIT_ASSERT(zs->avail_in == 0); + + if (file->compute_digest) + git_hash_update(&file->digest, source, len); + } + + return 0; +} + +#define MAX_SYMLINK_DEPTH 5 + +static int resolve_symlink(git_str *out, const char *path) +{ + int i, error, root; + ssize_t ret; + struct stat st; + git_str curpath = GIT_STR_INIT, target = GIT_STR_INIT; + + if ((error = git_str_grow(&target, GIT_PATH_MAX + 1)) < 0 || + (error = git_str_puts(&curpath, path)) < 0) + return error; + + for (i = 0; i < MAX_SYMLINK_DEPTH; i++) { + error = p_lstat(curpath.ptr, &st); + if (error < 0 && errno == ENOENT) { + error = git_str_puts(out, curpath.ptr); + goto cleanup; + } + + if (error < 0) { + git_error_set(GIT_ERROR_OS, "failed to stat '%s'", curpath.ptr); + error = -1; + goto cleanup; + } + + if (!S_ISLNK(st.st_mode)) { + error = git_str_puts(out, curpath.ptr); + goto cleanup; + } + + ret = p_readlink(curpath.ptr, target.ptr, GIT_PATH_MAX); + if (ret < 0) { + git_error_set(GIT_ERROR_OS, "failed to read symlink '%s'", curpath.ptr); + error = -1; + goto cleanup; + } + + if (ret == GIT_PATH_MAX) { + git_error_set(GIT_ERROR_INVALID, "symlink target too long"); + error = -1; + goto cleanup; + } + + /* readlink(2) won't NUL-terminate for us */ + target.ptr[ret] = '\0'; + target.size = ret; + + root = git_fs_path_root(target.ptr); + if (root >= 0) { + if ((error = git_str_sets(&curpath, target.ptr)) < 0) + goto cleanup; + } else { + git_str dir = GIT_STR_INIT; + + if ((error = git_fs_path_dirname_r(&dir, curpath.ptr)) < 0) + goto cleanup; + + git_str_swap(&curpath, &dir); + git_str_dispose(&dir); + + if ((error = git_fs_path_apply_relative(&curpath, target.ptr)) < 0) + goto cleanup; + } + } + + git_error_set(GIT_ERROR_INVALID, "maximum symlink depth reached"); + error = -1; + +cleanup: + git_str_dispose(&curpath); + git_str_dispose(&target); + return error; +} + +int git_filebuf_open(git_filebuf *file, const char *path, int flags, mode_t mode) +{ + return git_filebuf_open_withsize(file, path, flags, mode, WRITE_BUFFER_SIZE); +} + +int git_filebuf_open_withsize(git_filebuf *file, const char *path, int flags, mode_t mode, size_t size) +{ + int compression, error = -1; + size_t path_len, alloc_len; + + GIT_ASSERT_ARG(file); + GIT_ASSERT_ARG(path); + GIT_ASSERT(file->buffer == NULL); + + memset(file, 0x0, sizeof(git_filebuf)); + + if (flags & GIT_FILEBUF_DO_NOT_BUFFER) + file->do_not_buffer = true; + + if (flags & GIT_FILEBUF_FSYNC) + file->do_fsync = true; + + file->buf_size = size; + file->buf_pos = 0; + file->fd = -1; + file->last_error = BUFERR_OK; + + /* Allocate the main cache buffer */ + if (!file->do_not_buffer) { + file->buffer = git__malloc(file->buf_size); + GIT_ERROR_CHECK_ALLOC(file->buffer); + } + + /* If we are hashing on-write, allocate a new hash context */ + if (flags & GIT_FILEBUF_HASH_SHA1) { + file->compute_digest = 1; + + if (git_hash_ctx_init(&file->digest, GIT_HASH_ALGORITHM_SHA1) < 0) + goto cleanup; + } else if (flags & GIT_FILEBUF_HASH_SHA256) { + file->compute_digest = 1; + + if (git_hash_ctx_init(&file->digest, GIT_HASH_ALGORITHM_SHA256) < 0) + goto cleanup; + } + + compression = flags >> GIT_FILEBUF_DEFLATE_SHIFT; + + /* If we are deflating on-write, */ + if (compression != 0) { + /* Initialize the ZLib stream */ + if (deflateInit(&file->zs, compression) != Z_OK) { + git_error_set(GIT_ERROR_ZLIB, "failed to initialize zlib"); + goto cleanup; + } + + /* Allocate the Zlib cache buffer */ + file->z_buf = git__malloc(file->buf_size); + GIT_ERROR_CHECK_ALLOC(file->z_buf); + + /* Never flush */ + file->flush_mode = Z_NO_FLUSH; + file->write = &write_deflate; + } else { + file->write = &write_normal; + } + + /* If we are writing to a temp file */ + if (flags & GIT_FILEBUF_TEMPORARY) { + git_str tmp_path = GIT_STR_INIT; + + /* Open the file as temporary for locking */ + file->fd = git_futils_mktmp(&tmp_path, path, mode); + + if (file->fd < 0) { + git_str_dispose(&tmp_path); + goto cleanup; + } + file->fd_is_open = true; + file->created_lock = true; + + /* No original path */ + file->path_original = NULL; + file->path_lock = git_str_detach(&tmp_path); + GIT_ERROR_CHECK_ALLOC(file->path_lock); + } else { + git_str resolved_path = GIT_STR_INIT; + + if ((error = resolve_symlink(&resolved_path, path)) < 0) + goto cleanup; + + /* Save the original path of the file */ + path_len = resolved_path.size; + file->path_original = git_str_detach(&resolved_path); + + /* create the locking path by appending ".lock" to the original */ + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, path_len, GIT_FILELOCK_EXTLENGTH); + file->path_lock = git__malloc(alloc_len); + GIT_ERROR_CHECK_ALLOC(file->path_lock); + + memcpy(file->path_lock, file->path_original, path_len); + memcpy(file->path_lock + path_len, GIT_FILELOCK_EXTENSION, GIT_FILELOCK_EXTLENGTH); + + if (git_fs_path_isdir(file->path_original)) { + git_error_set(GIT_ERROR_FILESYSTEM, "path '%s' is a directory", file->path_original); + error = GIT_EDIRECTORY; + goto cleanup; + } + + /* open the file for locking */ + if ((error = lock_file(file, flags, mode)) < 0) + goto cleanup; + + file->created_lock = true; + } + + return 0; + +cleanup: + git_filebuf_cleanup(file); + return error; +} + +int git_filebuf_hash(unsigned char *out, git_filebuf *file) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(file); + GIT_ASSERT_ARG(file->compute_digest); + + flush_buffer(file); + + if (verify_last_error(file) < 0) + return -1; + + git_hash_final(out, &file->digest); + git_hash_ctx_cleanup(&file->digest); + file->compute_digest = 0; + + return 0; +} + +int git_filebuf_commit_at(git_filebuf *file, const char *path) +{ + git__free(file->path_original); + file->path_original = git__strdup(path); + GIT_ERROR_CHECK_ALLOC(file->path_original); + + return git_filebuf_commit(file); +} + +int git_filebuf_commit(git_filebuf *file) +{ + /* temporary files cannot be committed */ + GIT_ASSERT_ARG(file); + GIT_ASSERT(file->path_original); + + file->flush_mode = Z_FINISH; + flush_buffer(file); + + if (verify_last_error(file) < 0) + goto on_error; + + file->fd_is_open = false; + + if (file->do_fsync && p_fsync(file->fd) < 0) { + git_error_set(GIT_ERROR_OS, "failed to fsync '%s'", file->path_lock); + goto on_error; + } + + if (p_close(file->fd) < 0) { + git_error_set(GIT_ERROR_OS, "failed to close file at '%s'", file->path_lock); + goto on_error; + } + + file->fd = -1; + + if (p_rename(file->path_lock, file->path_original) < 0) { + git_error_set(GIT_ERROR_OS, "failed to rename lockfile to '%s'", file->path_original); + goto on_error; + } + + if (file->do_fsync && git_futils_fsync_parent(file->path_original) < 0) + goto on_error; + + file->did_rename = true; + + git_filebuf_cleanup(file); + return 0; + +on_error: + git_filebuf_cleanup(file); + return -1; +} + +GIT_INLINE(void) add_to_cache(git_filebuf *file, const void *buf, size_t len) +{ + memcpy(file->buffer + file->buf_pos, buf, len); + file->buf_pos += len; +} + +int git_filebuf_write(git_filebuf *file, const void *buff, size_t len) +{ + const unsigned char *buf = buff; + + ENSURE_BUF_OK(file); + + if (file->do_not_buffer) + return file->write(file, (void *)buff, len); + + for (;;) { + size_t space_left = file->buf_size - file->buf_pos; + + /* cache if it's small */ + if (space_left > len) { + add_to_cache(file, buf, len); + return 0; + } + + add_to_cache(file, buf, space_left); + if (flush_buffer(file) < 0) + return -1; + + len -= space_left; + buf += space_left; + } +} + +int git_filebuf_reserve(git_filebuf *file, void **buffer, size_t len) +{ + size_t space_left = file->buf_size - file->buf_pos; + + *buffer = NULL; + + ENSURE_BUF_OK(file); + + if (len > file->buf_size) { + file->last_error = BUFERR_MEM; + return -1; + } + + if (space_left <= len) { + if (flush_buffer(file) < 0) + return -1; + } + + *buffer = (file->buffer + file->buf_pos); + file->buf_pos += len; + + return 0; +} + +int git_filebuf_printf(git_filebuf *file, const char *format, ...) +{ + va_list arglist; + size_t space_left, len, alloclen; + int written, res; + char *tmp_buffer; + + ENSURE_BUF_OK(file); + + space_left = file->buf_size - file->buf_pos; + + do { + va_start(arglist, format); + written = p_vsnprintf((char *)file->buffer + file->buf_pos, space_left, format, arglist); + va_end(arglist); + + if (written < 0) { + file->last_error = BUFERR_MEM; + return -1; + } + + len = written; + if (len + 1 <= space_left) { + file->buf_pos += len; + return 0; + } + + if (flush_buffer(file) < 0) + return -1; + + space_left = file->buf_size - file->buf_pos; + + } while (len + 1 <= space_left); + + if (GIT_ADD_SIZET_OVERFLOW(&alloclen, len, 1) || + !(tmp_buffer = git__malloc(alloclen))) { + file->last_error = BUFERR_MEM; + return -1; + } + + va_start(arglist, format); + written = p_vsnprintf(tmp_buffer, len + 1, format, arglist); + va_end(arglist); + + if (written < 0) { + git__free(tmp_buffer); + file->last_error = BUFERR_MEM; + return -1; + } + + res = git_filebuf_write(file, tmp_buffer, len); + git__free(tmp_buffer); + + return res; +} + +int git_filebuf_stats(time_t *mtime, size_t *size, git_filebuf *file) +{ + int res; + struct stat st; + + if (file->fd_is_open) + res = p_fstat(file->fd, &st); + else + res = p_stat(file->path_original, &st); + + if (res < 0) { + git_error_set(GIT_ERROR_OS, "could not get stat info for '%s'", + file->path_original); + return res; + } + + if (mtime) + *mtime = st.st_mtime; + if (size) + *size = (size_t)st.st_size; + + return 0; +} diff --git a/src/util/filebuf.h b/src/util/filebuf.h new file mode 100644 index 0000000..e23b9ed --- /dev/null +++ b/src/util/filebuf.h @@ -0,0 +1,107 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_filebuf_h__ +#define INCLUDE_filebuf_h__ + +#include "git2_util.h" + +#include "futils.h" +#include "hash.h" +#include + +#ifdef GIT_THREADS +# define GIT_FILEBUF_THREADS +#endif + +#define GIT_FILEBUF_HASH_SHA1 (1 << 0) +#define GIT_FILEBUF_HASH_SHA256 (1 << 1) +#define GIT_FILEBUF_APPEND (1 << 2) +#define GIT_FILEBUF_CREATE_LEADING_DIRS (1 << 3) +#define GIT_FILEBUF_TEMPORARY (1 << 4) +#define GIT_FILEBUF_DO_NOT_BUFFER (1 << 5) +#define GIT_FILEBUF_FSYNC (1 << 6) +#define GIT_FILEBUF_DEFLATE_SHIFT (7) + +#define GIT_FILELOCK_EXTENSION ".lock\0" +#define GIT_FILELOCK_EXTLENGTH 6 + +typedef struct git_filebuf git_filebuf; +struct git_filebuf { + char *path_original; + char *path_lock; + + int (*write)(git_filebuf *file, void *source, size_t len); + + bool compute_digest; + git_hash_ctx digest; + + unsigned char *buffer; + unsigned char *z_buf; + + z_stream zs; + int flush_mode; + + size_t buf_size, buf_pos; + git_file fd; + bool fd_is_open; + bool created_lock; + bool did_rename; + bool do_not_buffer; + bool do_fsync; + int last_error; +}; + +#define GIT_FILEBUF_INIT {0} + +/* + * The git_filebuf object lifecycle is: + * - Allocate git_filebuf, preferably using GIT_FILEBUF_INIT. + * + * - Call git_filebuf_open() to initialize the filebuf for use. + * + * - Make as many calls to git_filebuf_write(), git_filebuf_printf(), + * git_filebuf_reserve() as you like. The error codes for these + * functions don't need to be checked. They are stored internally + * by the file buffer. + * + * - While you are writing, you may call git_filebuf_hash() to get + * the hash of all you have written so far. This function will + * fail if any of the previous writes to the buffer failed. + * + * - To close the git_filebuf, you may call git_filebuf_commit() or + * git_filebuf_commit_at() to save the file, or + * git_filebuf_cleanup() to abandon the file. All of these will + * free the git_filebuf object. Likewise, all of these will fail + * if any of the previous writes to the buffer failed, and set + * an error code accordingly. + */ +int git_filebuf_write(git_filebuf *lock, const void *buff, size_t len); +int git_filebuf_reserve(git_filebuf *file, void **buff, size_t len); +int git_filebuf_printf(git_filebuf *file, const char *format, ...) GIT_FORMAT_PRINTF(2, 3); + +int git_filebuf_open(git_filebuf *lock, const char *path, int flags, mode_t mode); +int git_filebuf_open_withsize(git_filebuf *file, const char *path, int flags, mode_t mode, size_t size); +int git_filebuf_commit(git_filebuf *lock); +int git_filebuf_commit_at(git_filebuf *lock, const char *path); +void git_filebuf_cleanup(git_filebuf *lock); +int git_filebuf_hash(unsigned char *out, git_filebuf *file); +int git_filebuf_flush(git_filebuf *file); +int git_filebuf_stats(time_t *mtime, size_t *size, git_filebuf *file); + +GIT_INLINE(int) git_filebuf_hash_flags(git_hash_algorithm_t algorithm) +{ + switch (algorithm) { + case GIT_HASH_ALGORITHM_SHA1: + return GIT_FILEBUF_HASH_SHA1; + case GIT_HASH_ALGORITHM_SHA256: + return GIT_FILEBUF_HASH_SHA256; + default: + return 0; + } +} + +#endif diff --git a/src/util/fs_path.c b/src/util/fs_path.c new file mode 100644 index 0000000..e03fcf7 --- /dev/null +++ b/src/util/fs_path.c @@ -0,0 +1,2066 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "fs_path.h" + +#include "git2_util.h" +#include "futils.h" +#include "posix.h" +#ifdef GIT_WIN32 +#include "win32/posix.h" +#include "win32/w32_buffer.h" +#include "win32/w32_util.h" +#include "win32/version.h" +#include +#else +#include +#endif +#include +#include + +#define ensure_error_set(code) do { \ + const git_error *e = git_error_last(); \ + if (!e || !e->message) \ + git_error_set(e ? e->klass : GIT_ERROR_CALLBACK, \ + "filesystem callback returned %d", code); \ + } while(0) + +static int dos_drive_prefix_length(const char *path) +{ + int i; + + /* + * Does it start with an ASCII letter (i.e. highest bit not set), + * followed by a colon? + */ + if (!(0x80 & (unsigned char)*path)) + return *path && path[1] == ':' ? 2 : 0; + + /* + * While drive letters must be letters of the English alphabet, it is + * possible to assign virtually _any_ Unicode character via `subst` as + * a drive letter to "virtual drives". Even `1`, or `ä`. Or fun stuff + * like this: + * + * subst ֍: %USERPROFILE%\Desktop + */ + for (i = 1; i < 4 && (0x80 & (unsigned char)path[i]); i++) + ; /* skip first UTF-8 character */ + return path[i] == ':' ? i + 1 : 0; +} + +#ifdef GIT_WIN32 +static bool looks_like_network_computer_name(const char *path, int pos) +{ + if (pos < 3) + return false; + + if (path[0] != '/' || path[1] != '/') + return false; + + while (pos-- > 2) { + if (path[pos] == '/') + return false; + } + + return true; +} +#endif + +/* + * Based on the Android implementation, BSD licensed. + * http://android.git.kernel.org/ + * + * Copyright (C) 2008 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +int git_fs_path_basename_r(git_str *buffer, const char *path) +{ + const char *endp, *startp; + int len, result; + + /* Empty or NULL string gets treated as "." */ + if (path == NULL || *path == '\0') { + startp = "."; + len = 1; + goto Exit; + } + + /* Strip trailing slashes */ + endp = path + strlen(path) - 1; + while (endp > path && *endp == '/') + endp--; + + /* All slashes becomes "/" */ + if (endp == path && *endp == '/') { + startp = "/"; + len = 1; + goto Exit; + } + + /* Find the start of the base */ + startp = endp; + while (startp > path && *(startp - 1) != '/') + startp--; + + /* Cast is safe because max path < max int */ + len = (int)(endp - startp + 1); + +Exit: + result = len; + + if (buffer != NULL && git_str_set(buffer, startp, len) < 0) + return -1; + + return result; +} + +/* + * Determine if the path is a Windows prefix and, if so, returns + * its actual length. If it is not a prefix, returns -1. + */ +static int win32_prefix_length(const char *path, int len) +{ +#ifndef GIT_WIN32 + GIT_UNUSED(path); + GIT_UNUSED(len); +#else + /* + * Mimic unix behavior where '/.git' returns '/': 'C:/.git' + * will return 'C:/' here + */ + if (dos_drive_prefix_length(path) == len) + return len; + + /* + * Similarly checks if we're dealing with a network computer name + * '//computername/.git' will return '//computername/' + */ + if (looks_like_network_computer_name(path, len)) + return len; +#endif + + return -1; +} + +/* + * Based on the Android implementation, BSD licensed. + * Check http://android.git.kernel.org/ + */ +int git_fs_path_dirname_r(git_str *buffer, const char *path) +{ + const char *endp; + int is_prefix = 0, len; + + /* Empty or NULL string gets treated as "." */ + if (path == NULL || *path == '\0') { + path = "."; + len = 1; + goto Exit; + } + + /* Strip trailing slashes */ + endp = path + strlen(path) - 1; + while (endp > path && *endp == '/') + endp--; + + if (endp - path + 1 > INT_MAX) { + git_error_set(GIT_ERROR_INVALID, "path too long"); + return -1; + } + + if ((len = win32_prefix_length(path, (int)(endp - path + 1))) > 0) { + is_prefix = 1; + goto Exit; + } + + /* Find the start of the dir */ + while (endp > path && *endp != '/') + endp--; + + /* Either the dir is "/" or there are no slashes */ + if (endp == path) { + path = (*endp == '/') ? "/" : "."; + len = 1; + goto Exit; + } + + do { + endp--; + } while (endp > path && *endp == '/'); + + if (endp - path + 1 > INT_MAX) { + git_error_set(GIT_ERROR_INVALID, "path too long"); + return -1; + } + + if ((len = win32_prefix_length(path, (int)(endp - path + 1))) > 0) { + is_prefix = 1; + goto Exit; + } + + /* Cast is safe because max path < max int */ + len = (int)(endp - path + 1); + +Exit: + if (buffer) { + if (git_str_set(buffer, path, len) < 0) + return -1; + if (is_prefix && git_str_putc(buffer, '/') < 0) + return -1; + } + + return len; +} + + +char *git_fs_path_dirname(const char *path) +{ + git_str buf = GIT_STR_INIT; + char *dirname; + + git_fs_path_dirname_r(&buf, path); + dirname = git_str_detach(&buf); + git_str_dispose(&buf); /* avoid memleak if error occurs */ + + return dirname; +} + +char *git_fs_path_basename(const char *path) +{ + git_str buf = GIT_STR_INIT; + char *basename; + + git_fs_path_basename_r(&buf, path); + basename = git_str_detach(&buf); + git_str_dispose(&buf); /* avoid memleak if error occurs */ + + return basename; +} + +size_t git_fs_path_basename_offset(git_str *buffer) +{ + ssize_t slash; + + if (!buffer || buffer->size <= 0) + return 0; + + slash = git_str_rfind_next(buffer, '/'); + + if (slash >= 0 && buffer->ptr[slash] == '/') + return (size_t)(slash + 1); + + return 0; +} + +int git_fs_path_root(const char *path) +{ + int offset = 0, prefix_len; + + /* Does the root of the path look like a windows drive ? */ + if ((prefix_len = dos_drive_prefix_length(path))) + offset += prefix_len; + +#ifdef GIT_WIN32 + /* Are we dealing with a windows network path? */ + else if ((path[0] == '/' && path[1] == '/' && path[2] != '/') || + (path[0] == '\\' && path[1] == '\\' && path[2] != '\\')) + { + offset += 2; + + /* Skip the computer name segment */ + while (path[offset] && path[offset] != '/' && path[offset] != '\\') + offset++; + } + + if (path[offset] == '\\') + return offset; +#endif + + if (path[offset] == '/') + return offset; + + return -1; /* Not a real error - signals that path is not rooted */ +} + +static void path_trim_slashes(git_str *path) +{ + int ceiling = git_fs_path_root(path->ptr) + 1; + + if (ceiling < 0) + return; + + while (path->size > (size_t)ceiling) { + if (path->ptr[path->size-1] != '/') + break; + + path->ptr[path->size-1] = '\0'; + path->size--; + } +} + +int git_fs_path_join_unrooted( + git_str *path_out, const char *path, const char *base, ssize_t *root_at) +{ + ssize_t root; + + GIT_ASSERT_ARG(path_out); + GIT_ASSERT_ARG(path); + + root = (ssize_t)git_fs_path_root(path); + + if (base != NULL && root < 0) { + if (git_str_joinpath(path_out, base, path) < 0) + return -1; + + root = (ssize_t)strlen(base); + } else { + if (git_str_sets(path_out, path) < 0) + return -1; + + if (root < 0) + root = 0; + else if (base) + git_fs_path_equal_or_prefixed(base, path, &root); + } + + if (root_at) + *root_at = root; + + return 0; +} + +void git_fs_path_squash_slashes(git_str *path) +{ + char *p, *q; + + if (path->size == 0) + return; + + for (p = path->ptr, q = path->ptr; *q; p++, q++) { + *p = *q; + + while (*q == '/' && *(q+1) == '/') { + path->size--; + q++; + } + } + + *p = '\0'; +} + +int git_fs_path_prettify(git_str *path_out, const char *path, const char *base) +{ + char buf[GIT_PATH_MAX]; + + GIT_ASSERT_ARG(path_out); + GIT_ASSERT_ARG(path); + + /* construct path if needed */ + if (base != NULL && git_fs_path_root(path) < 0) { + if (git_str_joinpath(path_out, base, path) < 0) + return -1; + path = path_out->ptr; + } + + if (p_realpath(path, buf) == NULL) { + /* git_error_set resets the errno when dealing with a GIT_ERROR_OS kind of error */ + int error = (errno == ENOENT || errno == ENOTDIR) ? GIT_ENOTFOUND : -1; + git_error_set(GIT_ERROR_OS, "failed to resolve path '%s'", path); + + git_str_clear(path_out); + + return error; + } + + return git_str_sets(path_out, buf); +} + +int git_fs_path_prettify_dir(git_str *path_out, const char *path, const char *base) +{ + int error = git_fs_path_prettify(path_out, path, base); + return (error < 0) ? error : git_fs_path_to_dir(path_out); +} + +int git_fs_path_to_dir(git_str *path) +{ + if (path->asize > 0 && + git_str_len(path) > 0 && + path->ptr[git_str_len(path) - 1] != '/') + git_str_putc(path, '/'); + + return git_str_oom(path) ? -1 : 0; +} + +void git_fs_path_string_to_dir(char *path, size_t size) +{ + size_t end = strlen(path); + + if (end && path[end - 1] != '/' && end < size) { + path[end] = '/'; + path[end + 1] = '\0'; + } +} + +int git__percent_decode(git_str *decoded_out, const char *input) +{ + int len, hi, lo, i; + + GIT_ASSERT_ARG(decoded_out); + GIT_ASSERT_ARG(input); + + len = (int)strlen(input); + git_str_clear(decoded_out); + + for(i = 0; i < len; i++) + { + char c = input[i]; + + if (c != '%') + goto append; + + if (i >= len - 2) + goto append; + + hi = git__fromhex(input[i + 1]); + lo = git__fromhex(input[i + 2]); + + if (hi < 0 || lo < 0) + goto append; + + c = (char)(hi << 4 | lo); + i += 2; + +append: + if (git_str_putc(decoded_out, c) < 0) + return -1; + } + + return 0; +} + +static int error_invalid_local_file_uri(const char *uri) +{ + git_error_set(GIT_ERROR_CONFIG, "'%s' is not a valid local file URI", uri); + return -1; +} + +static int local_file_url_prefixlen(const char *file_url) +{ + int len = -1; + + if (git__prefixcmp(file_url, "file://") == 0) { + if (file_url[7] == '/') + len = 8; + else if (git__prefixcmp(file_url + 7, "localhost/") == 0) + len = 17; + } + + return len; +} + +bool git_fs_path_is_local_file_url(const char *file_url) +{ + return (local_file_url_prefixlen(file_url) > 0); +} + +int git_fs_path_fromurl(git_str *local_path_out, const char *file_url) +{ + int offset; + + GIT_ASSERT_ARG(local_path_out); + GIT_ASSERT_ARG(file_url); + + if ((offset = local_file_url_prefixlen(file_url)) < 0 || + file_url[offset] == '\0' || file_url[offset] == '/') + return error_invalid_local_file_uri(file_url); + +#ifndef GIT_WIN32 + offset--; /* A *nix absolute path starts with a forward slash */ +#endif + + git_str_clear(local_path_out); + return git__percent_decode(local_path_out, file_url + offset); +} + +int git_fs_path_walk_up( + git_str *path, + const char *ceiling, + int (*cb)(void *data, const char *), + void *data) +{ + int error = 0; + git_str iter; + ssize_t stop = 0, scan; + char oldc = '\0'; + + GIT_ASSERT_ARG(path); + GIT_ASSERT_ARG(cb); + + if (ceiling != NULL) { + if (git__prefixcmp(path->ptr, ceiling) == 0) + stop = (ssize_t)strlen(ceiling); + else + stop = git_str_len(path); + } + scan = git_str_len(path); + + /* empty path: yield only once */ + if (!scan) { + error = cb(data, ""); + if (error) + ensure_error_set(error); + return error; + } + + iter.ptr = path->ptr; + iter.size = git_str_len(path); + iter.asize = path->asize; + + while (scan >= stop) { + error = cb(data, iter.ptr); + iter.ptr[scan] = oldc; + + if (error) { + ensure_error_set(error); + break; + } + + scan = git_str_rfind_next(&iter, '/'); + if (scan >= 0) { + scan++; + oldc = iter.ptr[scan]; + iter.size = scan; + iter.ptr[scan] = '\0'; + } + } + + if (scan >= 0) + iter.ptr[scan] = oldc; + + /* relative path: yield for the last component */ + if (!error && stop == 0 && iter.ptr[0] != '/') { + error = cb(data, ""); + if (error) + ensure_error_set(error); + } + + return error; +} + +bool git_fs_path_exists(const char *path) +{ + GIT_ASSERT_ARG_WITH_RETVAL(path, false); + return p_access(path, F_OK) == 0; +} + +bool git_fs_path_isdir(const char *path) +{ + struct stat st; + if (p_stat(path, &st) < 0) + return false; + + return S_ISDIR(st.st_mode) != 0; +} + +bool git_fs_path_isfile(const char *path) +{ + struct stat st; + + GIT_ASSERT_ARG_WITH_RETVAL(path, false); + if (p_stat(path, &st) < 0) + return false; + + return S_ISREG(st.st_mode) != 0; +} + +bool git_fs_path_islink(const char *path) +{ + struct stat st; + + GIT_ASSERT_ARG_WITH_RETVAL(path, false); + if (p_lstat(path, &st) < 0) + return false; + + return S_ISLNK(st.st_mode) != 0; +} + +#ifdef GIT_WIN32 + +bool git_fs_path_is_empty_dir(const char *path) +{ + git_win32_path filter_w; + bool empty = false; + + if (git_win32__findfirstfile_filter(filter_w, path)) { + WIN32_FIND_DATAW findData; + HANDLE hFind = FindFirstFileW(filter_w, &findData); + + /* FindFirstFile will fail if there are no children to the given + * path, which can happen if the given path is a file (and obviously + * has no children) or if the given path is an empty mount point. + * (Most directories have at least directory entries '.' and '..', + * but ridiculously another volume mounted in another drive letter's + * path space do not, and thus have nothing to enumerate.) If + * FindFirstFile fails, check if this is a directory-like thing + * (a mount point). + */ + if (hFind == INVALID_HANDLE_VALUE) + return git_fs_path_isdir(path); + + /* If the find handle was created successfully, then it's a directory */ + empty = true; + + do { + /* Allow the enumeration to return . and .. and still be considered + * empty. In the special case of drive roots (i.e. C:\) where . and + * .. do not occur, we can still consider the path to be an empty + * directory if there's nothing there. */ + if (!git_fs_path_is_dot_or_dotdotW(findData.cFileName)) { + empty = false; + break; + } + } while (FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } + + return empty; +} + +#else + +static int path_found_entry(void *payload, git_str *path) +{ + GIT_UNUSED(payload); + return !git_fs_path_is_dot_or_dotdot(path->ptr); +} + +bool git_fs_path_is_empty_dir(const char *path) +{ + int error; + git_str dir = GIT_STR_INIT; + + if (!git_fs_path_isdir(path)) + return false; + + if ((error = git_str_sets(&dir, path)) != 0) + git_error_clear(); + else + error = git_fs_path_direach(&dir, 0, path_found_entry, NULL); + + git_str_dispose(&dir); + + return !error; +} + +#endif + +int git_fs_path_set_error(int errno_value, const char *path, const char *action) +{ + switch (errno_value) { + case ENOENT: + case ENOTDIR: + git_error_set(GIT_ERROR_OS, "could not find '%s' to %s", path, action); + return GIT_ENOTFOUND; + + case EINVAL: + case ENAMETOOLONG: + git_error_set(GIT_ERROR_OS, "invalid path for filesystem '%s'", path); + return GIT_EINVALIDSPEC; + + case EEXIST: + git_error_set(GIT_ERROR_OS, "failed %s - '%s' already exists", action, path); + return GIT_EEXISTS; + + case EACCES: + git_error_set(GIT_ERROR_OS, "failed %s - '%s' is locked", action, path); + return GIT_ELOCKED; + + default: + git_error_set(GIT_ERROR_OS, "could not %s '%s'", action, path); + return -1; + } +} + +int git_fs_path_lstat(const char *path, struct stat *st) +{ + if (p_lstat(path, st) == 0) + return 0; + + return git_fs_path_set_error(errno, path, "stat"); +} + +static bool _check_dir_contents( + git_str *dir, + const char *sub, + bool (*predicate)(const char *)) +{ + bool result; + size_t dir_size = git_str_len(dir); + size_t sub_size = strlen(sub); + size_t alloc_size; + + /* leave base valid even if we could not make space for subdir */ + if (GIT_ADD_SIZET_OVERFLOW(&alloc_size, dir_size, sub_size) || + GIT_ADD_SIZET_OVERFLOW(&alloc_size, alloc_size, 2) || + git_str_try_grow(dir, alloc_size, false) < 0) + return false; + + /* save excursion */ + if (git_str_joinpath(dir, dir->ptr, sub) < 0) + return false; + + result = predicate(dir->ptr); + + /* restore path */ + git_str_truncate(dir, dir_size); + return result; +} + +bool git_fs_path_contains(git_str *dir, const char *item) +{ + return _check_dir_contents(dir, item, &git_fs_path_exists); +} + +bool git_fs_path_contains_dir(git_str *base, const char *subdir) +{ + return _check_dir_contents(base, subdir, &git_fs_path_isdir); +} + +bool git_fs_path_contains_file(git_str *base, const char *file) +{ + return _check_dir_contents(base, file, &git_fs_path_isfile); +} + +int git_fs_path_find_dir(git_str *dir) +{ + int error = 0; + char buf[GIT_PATH_MAX]; + + if (p_realpath(dir->ptr, buf) != NULL) + error = git_str_sets(dir, buf); + + /* call dirname if this is not a directory */ + if (!error) /* && git_fs_path_isdir(dir->ptr) == false) */ + error = (git_fs_path_dirname_r(dir, dir->ptr) < 0) ? -1 : 0; + + if (!error) + error = git_fs_path_to_dir(dir); + + return error; +} + +int git_fs_path_resolve_relative(git_str *path, size_t ceiling) +{ + char *base, *to, *from, *next; + size_t len; + + GIT_ERROR_CHECK_ALLOC_STR(path); + + if (ceiling > path->size) + ceiling = path->size; + + /* recognize drive prefixes, etc. that should not be backed over */ + if (ceiling == 0) + ceiling = git_fs_path_root(path->ptr) + 1; + + /* recognize URL prefixes that should not be backed over */ + if (ceiling == 0) { + for (next = path->ptr; *next && git__isalpha(*next); ++next); + if (next[0] == ':' && next[1] == '/' && next[2] == '/') + ceiling = (next + 3) - path->ptr; + } + + base = to = from = path->ptr + ceiling; + + while (*from) { + for (next = from; *next && *next != '/'; ++next); + + len = next - from; + + if (len == 1 && from[0] == '.') + /* do nothing with singleton dot */; + + else if (len == 2 && from[0] == '.' && from[1] == '.') { + /* error out if trying to up one from a hard base */ + if (to == base && ceiling != 0) { + git_error_set(GIT_ERROR_INVALID, + "cannot strip root component off url"); + return -1; + } + + /* no more path segments to strip, + * use '../' as a new base path */ + if (to == base) { + if (*next == '/') + len++; + + if (to != from) + memmove(to, from, len); + + to += len; + /* this is now the base, can't back up from a + * relative prefix */ + base = to; + } else { + /* back up a path segment */ + while (to > base && to[-1] == '/') to--; + while (to > base && to[-1] != '/') to--; + } + } else { + if (*next == '/' && *from != '/') + len++; + + if (to != from) + memmove(to, from, len); + + to += len; + } + + from += len; + + while (*from == '/') from++; + } + + *to = '\0'; + + path->size = to - path->ptr; + + return 0; +} + +int git_fs_path_apply_relative(git_str *target, const char *relpath) +{ + return git_str_joinpath(target, git_str_cstr(target), relpath) || + git_fs_path_resolve_relative(target, 0); +} + +int git_fs_path_cmp( + const char *name1, size_t len1, int isdir1, + const char *name2, size_t len2, int isdir2, + int (*compare)(const char *, const char *, size_t)) +{ + unsigned char c1, c2; + size_t len = len1 < len2 ? len1 : len2; + int cmp; + + cmp = compare(name1, name2, len); + if (cmp) + return cmp; + + c1 = name1[len]; + c2 = name2[len]; + + if (c1 == '\0' && isdir1) + c1 = '/'; + + if (c2 == '\0' && isdir2) + c2 = '/'; + + return (c1 < c2) ? -1 : (c1 > c2) ? 1 : 0; +} + +size_t git_fs_path_common_dirlen(const char *one, const char *two) +{ + const char *p, *q, *dirsep = NULL; + + for (p = one, q = two; *p && *q; p++, q++) { + if (*p == '/' && *q == '/') + dirsep = p; + else if (*p != *q) + break; + } + + return dirsep ? (dirsep - one) + 1 : 0; +} + +int git_fs_path_make_relative(git_str *path, const char *parent) +{ + const char *p, *q, *p_dirsep, *q_dirsep; + size_t plen = path->size, newlen, alloclen, depth = 1, i, offset; + + for (p_dirsep = p = path->ptr, q_dirsep = q = parent; *p && *q; p++, q++) { + if (*p == '/' && *q == '/') { + p_dirsep = p; + q_dirsep = q; + } + else if (*p != *q) + break; + } + + /* need at least 1 common path segment */ + if ((p_dirsep == path->ptr || q_dirsep == parent) && + (*p_dirsep != '/' || *q_dirsep != '/')) { + git_error_set(GIT_ERROR_INVALID, + "%s is not a parent of %s", parent, path->ptr); + return GIT_ENOTFOUND; + } + + if (*p == '/' && !*q) + p++; + else if (!*p && *q == '/') + q++; + else if (!*p && !*q) + return git_str_clear(path), 0; + else { + p = p_dirsep + 1; + q = q_dirsep + 1; + } + + plen -= (p - path->ptr); + + if (!*q) + return git_str_set(path, p, plen); + + for (; (q = strchr(q, '/')) && *(q + 1); q++) + depth++; + + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&newlen, depth, 3); + GIT_ERROR_CHECK_ALLOC_ADD(&newlen, newlen, plen); + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, newlen, 1); + + /* save the offset as we might realllocate the pointer */ + offset = p - path->ptr; + if (git_str_try_grow(path, alloclen, 1) < 0) + return -1; + p = path->ptr + offset; + + memmove(path->ptr + (depth * 3), p, plen + 1); + + for (i = 0; i < depth; i++) + memcpy(path->ptr + (i * 3), "../", 3); + + path->size = newlen; + return 0; +} + +bool git_fs_path_has_non_ascii(const char *path, size_t pathlen) +{ + const uint8_t *scan = (const uint8_t *)path, *end; + + for (end = scan + pathlen; scan < end; ++scan) + if (*scan & 0x80) + return true; + + return false; +} + +#ifdef GIT_USE_ICONV + +int git_fs_path_iconv_init_precompose(git_fs_path_iconv_t *ic) +{ + git_str_init(&ic->buf, 0); + ic->map = iconv_open(GIT_PATH_REPO_ENCODING, GIT_PATH_NATIVE_ENCODING); + return 0; +} + +void git_fs_path_iconv_clear(git_fs_path_iconv_t *ic) +{ + if (ic) { + if (ic->map != (iconv_t)-1) + iconv_close(ic->map); + git_str_dispose(&ic->buf); + } +} + +int git_fs_path_iconv(git_fs_path_iconv_t *ic, const char **in, size_t *inlen) +{ + char *nfd = (char*)*in, *nfc; + size_t nfdlen = *inlen, nfclen, wantlen = nfdlen, alloclen, rv; + int retry = 1; + + if (!ic || ic->map == (iconv_t)-1 || + !git_fs_path_has_non_ascii(*in, *inlen)) + return 0; + + git_str_clear(&ic->buf); + + while (1) { + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, wantlen, 1); + if (git_str_grow(&ic->buf, alloclen) < 0) + return -1; + + nfc = ic->buf.ptr + ic->buf.size; + nfclen = ic->buf.asize - ic->buf.size; + + rv = iconv(ic->map, &nfd, &nfdlen, &nfc, &nfclen); + + ic->buf.size = (nfc - ic->buf.ptr); + + if (rv != (size_t)-1) + break; + + /* if we cannot convert the data (probably because iconv thinks + * it is not valid UTF-8 source data), then use original data + */ + if (errno != E2BIG) + return 0; + + /* make space for 2x the remaining data to be converted + * (with per retry overhead to avoid infinite loops) + */ + wantlen = ic->buf.size + max(nfclen, nfdlen) * 2 + (size_t)(retry * 4); + + if (retry++ > 4) + goto fail; + } + + ic->buf.ptr[ic->buf.size] = '\0'; + + *in = ic->buf.ptr; + *inlen = ic->buf.size; + + return 0; + +fail: + git_error_set(GIT_ERROR_OS, "unable to convert unicode path data"); + return -1; +} + +static const char *nfc_file = "\xC3\x85\x73\x74\x72\xC3\xB6\x6D"; +static const char *nfd_file = "\x41\xCC\x8A\x73\x74\x72\x6F\xCC\x88\x6D"; + +/* Check if the platform is decomposing unicode data for us. We will + * emulate core Git and prefer to use precomposed unicode data internally + * on these platforms, composing the decomposed unicode on the fly. + * + * This mainly happens on the Mac where HDFS stores filenames as + * decomposed unicode. Even on VFAT and SAMBA file systems, the Mac will + * return decomposed unicode from readdir() even when the actual + * filesystem is storing precomposed unicode. + */ +bool git_fs_path_does_decompose_unicode(const char *root) +{ + git_str nfc_path = GIT_STR_INIT; + git_str nfd_path = GIT_STR_INIT; + int fd; + bool found_decomposed = false; + size_t orig_len; + const char *trailer; + + /* Create a file using a precomposed path and then try to find it + * using the decomposed name. If the lookup fails, then we will mark + * that we should precompose unicode for this repository. + */ + if (git_str_joinpath(&nfc_path, root, nfc_file) < 0) + goto done; + + /* record original path length before trailer */ + orig_len = nfc_path.size; + + if ((fd = git_futils_mktmp(&nfc_path, nfc_path.ptr, 0666)) < 0) + goto done; + p_close(fd); + + trailer = nfc_path.ptr + orig_len; + + /* try to look up as NFD path */ + if (git_str_joinpath(&nfd_path, root, nfd_file) < 0 || + git_str_puts(&nfd_path, trailer) < 0) + goto done; + + found_decomposed = git_fs_path_exists(nfd_path.ptr); + + /* remove temporary file (using original precomposed path) */ + (void)p_unlink(nfc_path.ptr); + +done: + git_str_dispose(&nfc_path); + git_str_dispose(&nfd_path); + return found_decomposed; +} + +#else + +bool git_fs_path_does_decompose_unicode(const char *root) +{ + GIT_UNUSED(root); + return false; +} + +#endif + +#if defined(__sun) || defined(__GNU__) +typedef char path_dirent_data[sizeof(struct dirent) + FILENAME_MAX + 1]; +#else +typedef struct dirent path_dirent_data; +#endif + +int git_fs_path_direach( + git_str *path, + uint32_t flags, + int (*fn)(void *, git_str *), + void *arg) +{ + int error = 0; + ssize_t wd_len; + DIR *dir; + struct dirent *de; + +#ifdef GIT_USE_ICONV + git_fs_path_iconv_t ic = GIT_PATH_ICONV_INIT; +#endif + + GIT_UNUSED(flags); + + if (git_fs_path_to_dir(path) < 0) + return -1; + + wd_len = git_str_len(path); + + if ((dir = opendir(path->ptr)) == NULL) { + git_error_set(GIT_ERROR_OS, "failed to open directory '%s'", path->ptr); + if (errno == ENOENT) + return GIT_ENOTFOUND; + + return -1; + } + +#ifdef GIT_USE_ICONV + if ((flags & GIT_FS_PATH_DIR_PRECOMPOSE_UNICODE) != 0) + (void)git_fs_path_iconv_init_precompose(&ic); +#endif + + while ((de = readdir(dir)) != NULL) { + const char *de_path = de->d_name; + size_t de_len = strlen(de_path); + + if (git_fs_path_is_dot_or_dotdot(de_path)) + continue; + +#ifdef GIT_USE_ICONV + if ((error = git_fs_path_iconv(&ic, &de_path, &de_len)) < 0) + break; +#endif + + if ((error = git_str_put(path, de_path, de_len)) < 0) + break; + + git_error_clear(); + error = fn(arg, path); + + git_str_truncate(path, wd_len); /* restore path */ + + /* Only set our own error if the callback did not set one already */ + if (error != 0) { + if (!git_error_last()) + ensure_error_set(error); + + break; + } + } + + closedir(dir); + +#ifdef GIT_USE_ICONV + git_fs_path_iconv_clear(&ic); +#endif + + return error; +} + +#if defined(GIT_WIN32) && !defined(__MINGW32__) + +/* Using _FIND_FIRST_EX_LARGE_FETCH may increase performance in Windows 7 + * and better. + */ +#ifndef FIND_FIRST_EX_LARGE_FETCH +# define FIND_FIRST_EX_LARGE_FETCH 2 +#endif + +int git_fs_path_diriter_init( + git_fs_path_diriter *diriter, + const char *path, + unsigned int flags) +{ + git_win32_path path_filter; + + static int is_win7_or_later = -1; + if (is_win7_or_later < 0) + is_win7_or_later = git_has_win32_version(6, 1, 0); + + GIT_ASSERT_ARG(diriter); + GIT_ASSERT_ARG(path); + + memset(diriter, 0, sizeof(git_fs_path_diriter)); + diriter->handle = INVALID_HANDLE_VALUE; + + if (git_str_puts(&diriter->path_utf8, path) < 0) + return -1; + + path_trim_slashes(&diriter->path_utf8); + + if (diriter->path_utf8.size == 0) { + git_error_set(GIT_ERROR_FILESYSTEM, "could not open directory '%s'", path); + return -1; + } + + if ((diriter->parent_len = git_win32_path_from_utf8(diriter->path, diriter->path_utf8.ptr)) < 0 || + !git_win32__findfirstfile_filter(path_filter, diriter->path_utf8.ptr)) { + git_error_set(GIT_ERROR_OS, "could not parse the directory path '%s'", path); + return -1; + } + + diriter->handle = FindFirstFileExW( + path_filter, + is_win7_or_later ? FindExInfoBasic : FindExInfoStandard, + &diriter->current, + FindExSearchNameMatch, + NULL, + is_win7_or_later ? FIND_FIRST_EX_LARGE_FETCH : 0); + + if (diriter->handle == INVALID_HANDLE_VALUE) { + git_error_set(GIT_ERROR_OS, "could not open directory '%s'", path); + return -1; + } + + diriter->parent_utf8_len = diriter->path_utf8.size; + diriter->flags = flags; + return 0; +} + +static int diriter_update_paths(git_fs_path_diriter *diriter) +{ + size_t filename_len, path_len; + + filename_len = wcslen(diriter->current.cFileName); + + if (GIT_ADD_SIZET_OVERFLOW(&path_len, diriter->parent_len, filename_len) || + GIT_ADD_SIZET_OVERFLOW(&path_len, path_len, 2)) + return -1; + + if (path_len > GIT_WIN_PATH_UTF16) { + git_error_set(GIT_ERROR_FILESYSTEM, + "invalid path '%.*ls\\%ls' (path too long)", + diriter->parent_len, diriter->path, diriter->current.cFileName); + return -1; + } + + diriter->path[diriter->parent_len] = L'\\'; + memcpy(&diriter->path[diriter->parent_len+1], + diriter->current.cFileName, filename_len * sizeof(wchar_t)); + diriter->path[path_len-1] = L'\0'; + + git_str_truncate(&diriter->path_utf8, diriter->parent_utf8_len); + + if (diriter->parent_utf8_len > 0 && + diriter->path_utf8.ptr[diriter->parent_utf8_len-1] != '/') + git_str_putc(&diriter->path_utf8, '/'); + + git_str_put_w(&diriter->path_utf8, diriter->current.cFileName, filename_len); + + if (git_str_oom(&diriter->path_utf8)) + return -1; + + return 0; +} + +int git_fs_path_diriter_next(git_fs_path_diriter *diriter) +{ + bool skip_dot = !(diriter->flags & GIT_FS_PATH_DIR_INCLUDE_DOT_AND_DOTDOT); + + do { + /* Our first time through, we already have the data from + * FindFirstFileW. Use it, otherwise get the next file. + */ + if (!diriter->needs_next) + diriter->needs_next = 1; + else if (!FindNextFileW(diriter->handle, &diriter->current)) + return GIT_ITEROVER; + } while (skip_dot && git_fs_path_is_dot_or_dotdotW(diriter->current.cFileName)); + + if (diriter_update_paths(diriter) < 0) + return -1; + + return 0; +} + +int git_fs_path_diriter_filename( + const char **out, + size_t *out_len, + git_fs_path_diriter *diriter) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(out_len); + GIT_ASSERT_ARG(diriter); + GIT_ASSERT(diriter->path_utf8.size > diriter->parent_utf8_len); + + *out = &diriter->path_utf8.ptr[diriter->parent_utf8_len+1]; + *out_len = diriter->path_utf8.size - diriter->parent_utf8_len - 1; + return 0; +} + +int git_fs_path_diriter_fullpath( + const char **out, + size_t *out_len, + git_fs_path_diriter *diriter) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(out_len); + GIT_ASSERT_ARG(diriter); + + *out = diriter->path_utf8.ptr; + *out_len = diriter->path_utf8.size; + return 0; +} + +int git_fs_path_diriter_stat(struct stat *out, git_fs_path_diriter *diriter) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(diriter); + + return git_win32__file_attribute_to_stat(out, + (WIN32_FILE_ATTRIBUTE_DATA *)&diriter->current, + diriter->path); +} + +void git_fs_path_diriter_free(git_fs_path_diriter *diriter) +{ + if (diriter == NULL) + return; + + git_str_dispose(&diriter->path_utf8); + + if (diriter->handle != INVALID_HANDLE_VALUE) { + FindClose(diriter->handle); + diriter->handle = INVALID_HANDLE_VALUE; + } +} + +#else + +int git_fs_path_diriter_init( + git_fs_path_diriter *diriter, + const char *path, + unsigned int flags) +{ + GIT_ASSERT_ARG(diriter); + GIT_ASSERT_ARG(path); + + memset(diriter, 0, sizeof(git_fs_path_diriter)); + + if (git_str_puts(&diriter->path, path) < 0) + return -1; + + path_trim_slashes(&diriter->path); + + if (diriter->path.size == 0) { + git_error_set(GIT_ERROR_FILESYSTEM, "could not open directory '%s'", path); + return -1; + } + + if ((diriter->dir = opendir(diriter->path.ptr)) == NULL) { + git_str_dispose(&diriter->path); + + git_error_set(GIT_ERROR_OS, "failed to open directory '%s'", path); + return -1; + } + +#ifdef GIT_USE_ICONV + if ((flags & GIT_FS_PATH_DIR_PRECOMPOSE_UNICODE) != 0) + (void)git_fs_path_iconv_init_precompose(&diriter->ic); +#endif + + diriter->parent_len = diriter->path.size; + diriter->flags = flags; + + return 0; +} + +int git_fs_path_diriter_next(git_fs_path_diriter *diriter) +{ + struct dirent *de; + const char *filename; + size_t filename_len; + bool skip_dot = !(diriter->flags & GIT_FS_PATH_DIR_INCLUDE_DOT_AND_DOTDOT); + int error = 0; + + GIT_ASSERT_ARG(diriter); + + errno = 0; + + do { + if ((de = readdir(diriter->dir)) == NULL) { + if (!errno) + return GIT_ITEROVER; + + git_error_set(GIT_ERROR_OS, + "could not read directory '%s'", diriter->path.ptr); + return -1; + } + } while (skip_dot && git_fs_path_is_dot_or_dotdot(de->d_name)); + + filename = de->d_name; + filename_len = strlen(filename); + +#ifdef GIT_USE_ICONV + if ((diriter->flags & GIT_FS_PATH_DIR_PRECOMPOSE_UNICODE) != 0 && + (error = git_fs_path_iconv(&diriter->ic, &filename, &filename_len)) < 0) + return error; +#endif + + git_str_truncate(&diriter->path, diriter->parent_len); + + if (diriter->parent_len > 0 && + diriter->path.ptr[diriter->parent_len-1] != '/') + git_str_putc(&diriter->path, '/'); + + git_str_put(&diriter->path, filename, filename_len); + + if (git_str_oom(&diriter->path)) + return -1; + + return error; +} + +int git_fs_path_diriter_filename( + const char **out, + size_t *out_len, + git_fs_path_diriter *diriter) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(out_len); + GIT_ASSERT_ARG(diriter); + GIT_ASSERT(diriter->path.size > diriter->parent_len); + + *out = &diriter->path.ptr[diriter->parent_len+1]; + *out_len = diriter->path.size - diriter->parent_len - 1; + return 0; +} + +int git_fs_path_diriter_fullpath( + const char **out, + size_t *out_len, + git_fs_path_diriter *diriter) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(out_len); + GIT_ASSERT_ARG(diriter); + + *out = diriter->path.ptr; + *out_len = diriter->path.size; + return 0; +} + +int git_fs_path_diriter_stat(struct stat *out, git_fs_path_diriter *diriter) +{ + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(diriter); + + return git_fs_path_lstat(diriter->path.ptr, out); +} + +void git_fs_path_diriter_free(git_fs_path_diriter *diriter) +{ + if (diriter == NULL) + return; + + if (diriter->dir) { + closedir(diriter->dir); + diriter->dir = NULL; + } + +#ifdef GIT_USE_ICONV + git_fs_path_iconv_clear(&diriter->ic); +#endif + + git_str_dispose(&diriter->path); +} + +#endif + +int git_fs_path_dirload( + git_vector *contents, + const char *path, + size_t prefix_len, + uint32_t flags) +{ + git_fs_path_diriter iter = GIT_FS_PATH_DIRITER_INIT; + const char *name; + size_t name_len; + char *dup; + int error; + + GIT_ASSERT_ARG(contents); + GIT_ASSERT_ARG(path); + + if ((error = git_fs_path_diriter_init(&iter, path, flags)) < 0) + return error; + + while ((error = git_fs_path_diriter_next(&iter)) == 0) { + if ((error = git_fs_path_diriter_fullpath(&name, &name_len, &iter)) < 0) + break; + + GIT_ASSERT(name_len > prefix_len); + + dup = git__strndup(name + prefix_len, name_len - prefix_len); + GIT_ERROR_CHECK_ALLOC(dup); + + if ((error = git_vector_insert(contents, dup)) < 0) + break; + } + + if (error == GIT_ITEROVER) + error = 0; + + git_fs_path_diriter_free(&iter); + return error; +} + +int git_fs_path_from_url_or_path(git_str *local_path_out, const char *url_or_path) +{ + if (git_fs_path_is_local_file_url(url_or_path)) + return git_fs_path_fromurl(local_path_out, url_or_path); + else + return git_str_sets(local_path_out, url_or_path); +} + +/* Reject paths like AUX or COM1, or those versions that end in a dot or + * colon. ("AUX." or "AUX:") + */ +GIT_INLINE(bool) validate_dospath( + const char *component, + size_t len, + const char dospath[3], + bool trailing_num) +{ + size_t last = trailing_num ? 4 : 3; + + if (len < last || git__strncasecmp(component, dospath, 3) != 0) + return true; + + if (trailing_num && (component[3] < '1' || component[3] > '9')) + return true; + + return (len > last && + component[last] != '.' && + component[last] != ':'); +} + +GIT_INLINE(bool) validate_char(unsigned char c, unsigned int flags) +{ + if ((flags & GIT_FS_PATH_REJECT_BACKSLASH) && c == '\\') + return false; + + if ((flags & GIT_FS_PATH_REJECT_SLASH) && c == '/') + return false; + + if (flags & GIT_FS_PATH_REJECT_NT_CHARS) { + if (c < 32) + return false; + + switch (c) { + case '<': + case '>': + case ':': + case '"': + case '|': + case '?': + case '*': + return false; + } + } + + return true; +} + +/* + * We fundamentally don't like some paths when dealing with user-inputted + * strings (to avoid escaping a sandbox): we don't want dot or dot-dot + * anywhere, we want to avoid writing weird paths on Windows that can't + * be handled by tools that use the non-\\?\ APIs, we don't want slashes + * or double slashes at the end of paths that can make them ambiguous. + * + * For checkout, we don't want to recurse into ".git" either. + */ +static bool validate_component( + const char *component, + size_t len, + unsigned int flags) +{ + if (len == 0) + return !(flags & GIT_FS_PATH_REJECT_EMPTY_COMPONENT); + + if ((flags & GIT_FS_PATH_REJECT_TRAVERSAL) && + len == 1 && component[0] == '.') + return false; + + if ((flags & GIT_FS_PATH_REJECT_TRAVERSAL) && + len == 2 && component[0] == '.' && component[1] == '.') + return false; + + if ((flags & GIT_FS_PATH_REJECT_TRAILING_DOT) && + component[len - 1] == '.') + return false; + + if ((flags & GIT_FS_PATH_REJECT_TRAILING_SPACE) && + component[len - 1] == ' ') + return false; + + if ((flags & GIT_FS_PATH_REJECT_TRAILING_COLON) && + component[len - 1] == ':') + return false; + + if (flags & GIT_FS_PATH_REJECT_DOS_PATHS) { + if (!validate_dospath(component, len, "CON", false) || + !validate_dospath(component, len, "PRN", false) || + !validate_dospath(component, len, "AUX", false) || + !validate_dospath(component, len, "NUL", false) || + !validate_dospath(component, len, "COM", true) || + !validate_dospath(component, len, "LPT", true)) + return false; + } + + return true; +} + +#ifdef GIT_WIN32 +GIT_INLINE(bool) validate_length( + const char *path, + size_t len, + size_t utf8_char_len) +{ + GIT_UNUSED(path); + GIT_UNUSED(len); + + return (utf8_char_len <= MAX_PATH); +} +#endif + +bool git_fs_path_str_is_valid_ext( + const git_str *path, + unsigned int flags, + bool (*validate_char_cb)(char ch, void *payload), + bool (*validate_component_cb)(const char *component, size_t len, void *payload), + bool (*validate_length_cb)(const char *path, size_t len, size_t utf8_char_len), + void *payload) +{ + const char *start, *c; + size_t len = 0; + + if (!flags) + return true; + + for (start = c = path->ptr; *c && len < path->size; c++, len++) { + if (!validate_char(*c, flags)) + return false; + + if (validate_char_cb && !validate_char_cb(*c, payload)) + return false; + + if (*c != '/') + continue; + + if (!validate_component(start, (c - start), flags)) + return false; + + if (validate_component_cb && + !validate_component_cb(start, (c - start), payload)) + return false; + + start = c + 1; + } + + /* + * We want to support paths specified as either `const char *` + * or `git_str *`; we pass size as `SIZE_MAX` when we use a + * `const char *` to avoid a `strlen`. Ensure that we didn't + * have a NUL in the buffer if there was a non-SIZE_MAX length. + */ + if (path->size != SIZE_MAX && len != path->size) + return false; + + if (!validate_component(start, (c - start), flags)) + return false; + + if (validate_component_cb && + !validate_component_cb(start, (c - start), payload)) + return false; + +#ifdef GIT_WIN32 + if ((flags & GIT_FS_PATH_REJECT_LONG_PATHS) != 0) { + size_t utf8_len = git_utf8_char_length(path->ptr, len); + + if (!validate_length(path->ptr, len, utf8_len)) + return false; + + if (validate_length_cb && + !validate_length_cb(path->ptr, len, utf8_len)) + return false; + } +#else + GIT_UNUSED(validate_length_cb); +#endif + + return true; +} + +int git_fs_path_validate_str_length_with_suffix( + git_str *path, + size_t suffix_len) +{ +#ifdef GIT_WIN32 + size_t utf8_len = git_utf8_char_length(path->ptr, path->size); + size_t total_len; + + if (GIT_ADD_SIZET_OVERFLOW(&total_len, utf8_len, suffix_len) || + total_len > MAX_PATH) { + + git_error_set(GIT_ERROR_FILESYSTEM, "path too long: '%.*s'", + (int)path->size, path->ptr); + return -1; + } +#else + GIT_UNUSED(path); + GIT_UNUSED(suffix_len); +#endif + + return 0; +} + +int git_fs_path_normalize_slashes(git_str *out, const char *path) +{ + int error; + char *p; + + if ((error = git_str_puts(out, path)) < 0) + return error; + + for (p = out->ptr; *p; p++) { + if (*p == '\\') + *p = '/'; + } + + return 0; +} + +bool git_fs_path_supports_symlinks(const char *dir) +{ + git_str path = GIT_STR_INIT; + bool supported = false; + struct stat st; + int fd; + + if ((fd = git_futils_mktmp(&path, dir, 0666)) < 0 || + p_close(fd) < 0 || + p_unlink(path.ptr) < 0 || + p_symlink("testing", path.ptr) < 0 || + p_lstat(path.ptr, &st) < 0) + goto done; + + supported = (S_ISLNK(st.st_mode) != 0); +done: + if (path.size) + (void)p_unlink(path.ptr); + git_str_dispose(&path); + return supported; +} + +static git_fs_path_owner_t mock_owner = GIT_FS_PATH_OWNER_NONE; + +void git_fs_path__set_owner(git_fs_path_owner_t owner) +{ + mock_owner = owner; +} + +#ifdef GIT_WIN32 +static PSID *sid_dup(PSID sid) +{ + DWORD len; + PSID dup; + + len = GetLengthSid(sid); + + if ((dup = git__malloc(len)) == NULL) + return NULL; + + if (!CopySid(len, dup, sid)) { + git_error_set(GIT_ERROR_OS, "could not duplicate sid"); + git__free(dup); + return NULL; + } + + return dup; +} + +static int current_user_sid(PSID *out) +{ + TOKEN_USER *info = NULL; + HANDLE token = NULL; + DWORD len = 0; + int error = -1; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { + git_error_set(GIT_ERROR_OS, "could not lookup process information"); + goto done; + } + + if (GetTokenInformation(token, TokenUser, NULL, 0, &len) || + GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + git_error_set(GIT_ERROR_OS, "could not lookup token metadata"); + goto done; + } + + info = git__malloc(len); + GIT_ERROR_CHECK_ALLOC(info); + + if (!GetTokenInformation(token, TokenUser, info, len, &len)) { + git_error_set(GIT_ERROR_OS, "could not lookup current user"); + goto done; + } + + if ((*out = sid_dup(info->User.Sid))) + error = 0; + +done: + if (token) + CloseHandle(token); + + git__free(info); + return error; +} + +static int file_owner_sid(PSID *out, const char *path) +{ + git_win32_path path_w32; + PSECURITY_DESCRIPTOR descriptor = NULL; + PSID owner_sid; + DWORD ret; + int error = GIT_EINVALID; + + if (git_win32_path_from_utf8(path_w32, path) < 0) + return -1; + + ret = GetNamedSecurityInfoW(path_w32, SE_FILE_OBJECT, + OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, + &owner_sid, NULL, NULL, NULL, &descriptor); + + if (ret == ERROR_FILE_NOT_FOUND || ret == ERROR_PATH_NOT_FOUND) + error = GIT_ENOTFOUND; + else if (ret != ERROR_SUCCESS) + git_error_set(GIT_ERROR_OS, "failed to get security information"); + else if (!IsValidSid(owner_sid)) + git_error_set(GIT_ERROR_OS, "file owner is not valid"); + else if ((*out = sid_dup(owner_sid))) + error = 0; + + if (descriptor) + LocalFree(descriptor); + + return error; +} + +int git_fs_path_owner_is( + bool *out, + const char *path, + git_fs_path_owner_t owner_type) +{ + PSID owner_sid = NULL, user_sid = NULL; + BOOL is_admin, admin_owned; + int error; + + if (mock_owner) { + *out = ((mock_owner & owner_type) != 0); + return 0; + } + + if ((error = file_owner_sid(&owner_sid, path)) < 0) + goto done; + + if ((owner_type & GIT_FS_PATH_OWNER_CURRENT_USER) != 0) { + if ((error = current_user_sid(&user_sid)) < 0) + goto done; + + if (EqualSid(owner_sid, user_sid)) { + *out = true; + goto done; + } + } + + admin_owned = + IsWellKnownSid(owner_sid, WinBuiltinAdministratorsSid) || + IsWellKnownSid(owner_sid, WinLocalSystemSid); + + if (admin_owned && + (owner_type & GIT_FS_PATH_OWNER_ADMINISTRATOR) != 0) { + *out = true; + goto done; + } + + if (admin_owned && + (owner_type & GIT_FS_PATH_USER_IS_ADMINISTRATOR) != 0 && + CheckTokenMembership(NULL, owner_sid, &is_admin) && + is_admin) { + *out = true; + goto done; + } + + *out = false; + +done: + git__free(owner_sid); + git__free(user_sid); + return error; +} + +#else + +static int sudo_uid_lookup(uid_t *out) +{ + git_str uid_str = GIT_STR_INIT; + int64_t uid; + int error; + + if ((error = git__getenv(&uid_str, "SUDO_UID")) == 0 && + (error = git__strntol64(&uid, uid_str.ptr, uid_str.size, NULL, 10)) == 0 && + uid == (int64_t)((uid_t)uid)) { + *out = (uid_t)uid; + } + + git_str_dispose(&uid_str); + return error; +} + +int git_fs_path_owner_is( + bool *out, + const char *path, + git_fs_path_owner_t owner_type) +{ + struct stat st; + uid_t euid, sudo_uid; + + if (mock_owner) { + *out = ((mock_owner & owner_type) != 0); + return 0; + } + + euid = geteuid(); + + if (p_lstat(path, &st) != 0) { + if (errno == ENOENT) + return GIT_ENOTFOUND; + + git_error_set(GIT_ERROR_OS, "could not stat '%s'", path); + return -1; + } + + if ((owner_type & GIT_FS_PATH_OWNER_CURRENT_USER) != 0 && + st.st_uid == euid) { + *out = true; + return 0; + } + + if ((owner_type & GIT_FS_PATH_OWNER_ADMINISTRATOR) != 0 && + st.st_uid == 0) { + *out = true; + return 0; + } + + if ((owner_type & GIT_FS_PATH_OWNER_RUNNING_SUDO) != 0 && + euid == 0 && + sudo_uid_lookup(&sudo_uid) == 0 && + st.st_uid == sudo_uid) { + *out = true; + return 0; + } + + *out = false; + return 0; +} + +#endif + +int git_fs_path_owner_is_current_user(bool *out, const char *path) +{ + return git_fs_path_owner_is(out, path, GIT_FS_PATH_OWNER_CURRENT_USER); +} + +int git_fs_path_owner_is_system(bool *out, const char *path) +{ + return git_fs_path_owner_is(out, path, GIT_FS_PATH_OWNER_ADMINISTRATOR); +} + +int git_fs_path_find_executable(git_str *fullpath, const char *executable) +{ +#ifdef GIT_WIN32 + git_win32_path fullpath_w, executable_w; + int error; + + if (git_utf8_to_16(executable_w, GIT_WIN_PATH_MAX, executable) < 0) + return -1; + + error = git_win32_path_find_executable(fullpath_w, executable_w); + + if (error == 0) + error = git_str_put_w(fullpath, fullpath_w, wcslen(fullpath_w)); + + return error; +#else + git_str path = GIT_STR_INIT; + const char *current_dir, *term; + bool found = false; + + if (git__getenv(&path, "PATH") < 0) + return -1; + + current_dir = path.ptr; + + while (*current_dir) { + if (! (term = strchr(current_dir, GIT_PATH_LIST_SEPARATOR))) + term = strchr(current_dir, '\0'); + + git_str_clear(fullpath); + if (git_str_put(fullpath, current_dir, (term - current_dir)) < 0 || + git_str_putc(fullpath, '/') < 0 || + git_str_puts(fullpath, executable) < 0) + return -1; + + if (git_fs_path_isfile(fullpath->ptr)) { + found = true; + break; + } + + current_dir = term; + + while (*current_dir == GIT_PATH_LIST_SEPARATOR) + current_dir++; + } + + git_str_dispose(&path); + + if (found) + return 0; + + git_str_clear(fullpath); + return GIT_ENOTFOUND; +#endif +} diff --git a/src/util/fs_path.h b/src/util/fs_path.h new file mode 100644 index 0000000..e5ca673 --- /dev/null +++ b/src/util/fs_path.h @@ -0,0 +1,790 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_fs_path_h__ +#define INCLUDE_fs_path_h__ + +#include "git2_util.h" + +#include "posix.h" +#include "str.h" +#include "vector.h" +#include "utf8.h" + +/** + * Path manipulation utils + * + * These are path utilities that munge paths without actually + * looking at the real filesystem. + */ + +/* + * The dirname() function shall take a pointer to a character string + * that contains a pathname, and return a pointer to a string that is a + * pathname of the parent directory of that file. Trailing '/' characters + * in the path are not counted as part of the path. + * + * If path does not contain a '/', then dirname() shall return a pointer to + * the string ".". If path is a null pointer or points to an empty string, + * dirname() shall return a pointer to the string "." . + * + * The `git_fs_path_dirname` implementation is thread safe. The returned + * string must be manually free'd. + * + * The `git_fs_path_dirname_r` implementation writes the dirname to a `git_str` + * if the buffer pointer is not NULL. + * It returns an error code < 0 if there is an allocation error, otherwise + * the length of the dirname (which will be > 0). + */ +extern char *git_fs_path_dirname(const char *path); +extern int git_fs_path_dirname_r(git_str *buffer, const char *path); + +/* + * This function returns the basename of the file, which is the last + * part of its full name given by fname, with the drive letter and + * leading directories stripped off. For example, the basename of + * c:/foo/bar/file.ext is file.ext, and the basename of a:foo is foo. + * + * Trailing slashes and backslashes are significant: the basename of + * c:/foo/bar/ is an empty string after the rightmost slash. + * + * The `git_fs_path_basename` implementation is thread safe. The returned + * string must be manually free'd. + * + * The `git_fs_path_basename_r` implementation writes the basename to a `git_str`. + * It returns an error code < 0 if there is an allocation error, otherwise + * the length of the basename (which will be >= 0). + */ +extern char *git_fs_path_basename(const char *path); +extern int git_fs_path_basename_r(git_str *buffer, const char *path); + +/* Return the offset of the start of the basename. Unlike the other + * basename functions, this returns 0 if the path is empty. + */ +extern size_t git_fs_path_basename_offset(git_str *buffer); + +/** + * Find offset to root of path if path has one. + * + * This will return a number >= 0 which is the offset to the start of the + * path, if the path is rooted (i.e. "/rooted/path" returns 0 and + * "c:/windows/rooted/path" returns 2). If the path is not rooted, this + * returns -1. + */ +extern int git_fs_path_root(const char *path); + +/** + * Ensure path has a trailing '/'. + */ +extern int git_fs_path_to_dir(git_str *path); + +/** + * Ensure string has a trailing '/' if there is space for it. + */ +extern void git_fs_path_string_to_dir(char *path, size_t size); + +/** + * Taken from git.git; returns nonzero if the given path is "." or "..". + */ +GIT_INLINE(int) git_fs_path_is_dot_or_dotdot(const char *name) +{ + return (name[0] == '.' && + (name[1] == '\0' || + (name[1] == '.' && name[2] == '\0'))); +} + +#ifdef GIT_WIN32 +GIT_INLINE(int) git_fs_path_is_dot_or_dotdotW(const wchar_t *name) +{ + return (name[0] == L'.' && + (name[1] == L'\0' || + (name[1] == L'.' && name[2] == L'\0'))); +} + +#define git_fs_path_is_absolute(p) \ + (git__isalpha((p)[0]) && (p)[1] == ':' && ((p)[2] == '\\' || (p)[2] == '/')) + +#define git_fs_path_is_dirsep(p) \ + ((p) == '/' || (p) == '\\') + +/** + * Convert backslashes in path to forward slashes. + */ +GIT_INLINE(void) git_fs_path_mkposix(char *path) +{ + while (*path) { + if (*path == '\\') + *path = '/'; + + path++; + } +} +#else +# define git_fs_path_mkposix(p) /* blank */ + +#define git_fs_path_is_absolute(p) \ + ((p)[0] == '/') + +#define git_fs_path_is_dirsep(p) \ + ((p) == '/') + +#endif + +/** + * Check if string is a relative path (i.e. starts with "./" or "../") + */ +GIT_INLINE(int) git_fs_path_is_relative(const char *p) +{ + return (p[0] == '.' && (p[1] == '/' || (p[1] == '.' && p[2] == '/'))); +} + +/** + * Check if string is at end of path segment (i.e. looking at '/' or '\0') + */ +GIT_INLINE(int) git_fs_path_at_end_of_segment(const char *p) +{ + return !*p || *p == '/'; +} + +extern int git__percent_decode(git_str *decoded_out, const char *input); + +/** + * Extract path from file:// URL. + */ +extern int git_fs_path_fromurl(git_str *local_path_out, const char *file_url); + + +/** + * Path filesystem utils + * + * These are path utilities that actually access the filesystem. + */ + +/** + * Check if a file exists and can be accessed. + * @return true or false + */ +extern bool git_fs_path_exists(const char *path); + +/** + * Check if the given path points to a directory. + * @return true or false + */ +extern bool git_fs_path_isdir(const char *path); + +/** + * Check if the given path points to a regular file. + * @return true or false + */ +extern bool git_fs_path_isfile(const char *path); + +/** + * Check if the given path points to a symbolic link. + * @return true or false + */ +extern bool git_fs_path_islink(const char *path); + +/** + * Check if the given path is a directory, and is empty. + */ +extern bool git_fs_path_is_empty_dir(const char *path); + +/** + * Stat a file and/or link and set error if needed. + */ +extern int git_fs_path_lstat(const char *path, struct stat *st); + +/** + * Check if the parent directory contains the item. + * + * @param dir Directory to check. + * @param item Item that might be in the directory. + * @return 0 if item exists in directory, <0 otherwise. + */ +extern bool git_fs_path_contains(git_str *dir, const char *item); + +/** + * Check if the given path contains the given subdirectory. + * + * @param parent Directory path that might contain subdir + * @param subdir Subdirectory name to look for in parent + * @return true if subdirectory exists, false otherwise. + */ +extern bool git_fs_path_contains_dir(git_str *parent, const char *subdir); + +/** + * Determine the common directory length between two paths, including + * the final path separator. For example, given paths 'a/b/c/1.txt + * and 'a/b/c/d/2.txt', the common directory is 'a/b/c/', and this + * will return the length of the string 'a/b/c/', which is 6. + * + * @param one The first path + * @param two The second path + * @return The length of the common directory + */ +extern size_t git_fs_path_common_dirlen(const char *one, const char *two); + +/** + * Make the path relative to the given parent path. + * + * @param path The path to make relative + * @param parent The parent path to make path relative to + * @return 0 if path was made relative, GIT_ENOTFOUND + * if there was not common root between the paths, + * or <0. + */ +extern int git_fs_path_make_relative(git_str *path, const char *parent); + +/** + * Check if the given path contains the given file. + * + * @param dir Directory path that might contain file + * @param file File name to look for in parent + * @return true if file exists, false otherwise. + */ +extern bool git_fs_path_contains_file(git_str *dir, const char *file); + +/** + * Prepend base to unrooted path or just copy path over. + * + * This will optionally return the index into the path where the "root" + * is, either the end of the base directory prefix or the path root. + */ +extern int git_fs_path_join_unrooted( + git_str *path_out, const char *path, const char *base, ssize_t *root_at); + +/** + * Removes multiple occurrences of '/' in a row, squashing them into a + * single '/'. + */ +extern void git_fs_path_squash_slashes(git_str *path); + +/** + * Clean up path, prepending base if it is not already rooted. + */ +extern int git_fs_path_prettify(git_str *path_out, const char *path, const char *base); + +/** + * Clean up path, prepending base if it is not already rooted and + * appending a slash. + */ +extern int git_fs_path_prettify_dir(git_str *path_out, const char *path, const char *base); + +/** + * Get a directory from a path. + * + * If path is a directory, this acts like `git_fs_path_prettify_dir` + * (cleaning up path and appending a '/'). If path is a normal file, + * this prettifies it, then removed the filename a la dirname and + * appends the trailing '/'. If the path does not exist, it is + * treated like a regular filename. + */ +extern int git_fs_path_find_dir(git_str *dir); + +/** + * Resolve relative references within a path. + * + * This eliminates "./" and "../" relative references inside a path, + * as well as condensing multiple slashes into single ones. It will + * not touch the path before the "ceiling" length. + * + * Additionally, this will recognize an "c:/" drive prefix or a "xyz://" URL + * prefix and not touch that part of the path. + */ +extern int git_fs_path_resolve_relative(git_str *path, size_t ceiling); + +/** + * Apply a relative path to base path. + * + * Note that the base path could be a filename or a URL and this + * should still work. The relative path is walked segment by segment + * with three rules: series of slashes will be condensed to a single + * slash, "." will be eaten with no change, and ".." will remove a + * segment from the base path. + */ +extern int git_fs_path_apply_relative(git_str *target, const char *relpath); + +enum { + GIT_FS_PATH_DIR_IGNORE_CASE = (1u << 0), + GIT_FS_PATH_DIR_PRECOMPOSE_UNICODE = (1u << 1), + GIT_FS_PATH_DIR_INCLUDE_DOT_AND_DOTDOT = (1u << 2), +}; + +/** + * Walk each directory entry, except '.' and '..', calling fn(state). + * + * @param pathbuf Buffer the function reads the initial directory + * path from, and updates with each successive entry's name. + * @param flags Combination of GIT_FS_PATH_DIR flags. + * @param callback Callback for each entry. Passed the `payload` and each + * successive path inside the directory as a full path. This may + * safely append text to the pathbuf if needed. Return non-zero to + * cancel iteration (and return value will be propagated back). + * @param payload Passed to callback as first argument. + * @return 0 on success or error code from OS error or from callback + */ +extern int git_fs_path_direach( + git_str *pathbuf, + uint32_t flags, + int (*callback)(void *payload, git_str *path), + void *payload); + +/** + * Sort function to order two paths + */ +extern int git_fs_path_cmp( + const char *name1, size_t len1, int isdir1, + const char *name2, size_t len2, int isdir2, + int (*compare)(const char *, const char *, size_t)); + +/** + * Invoke callback up path directory by directory until the ceiling is + * reached (inclusive of a final call at the root_path). + * + * Returning anything other than 0 from the callback function + * will stop the iteration and propagate the error to the caller. + * + * @param pathbuf Buffer the function reads the directory from and + * and updates with each successive name. + * @param ceiling Prefix of path at which to stop walking up. If NULL, + * this will walk all the way up to the root. If not a prefix of + * pathbuf, the callback will be invoked a single time on the + * original input path. + * @param callback Function to invoke on each path. Passed the `payload` + * and the buffer containing the current path. The path should not + * be modified in any way. Return non-zero to stop iteration. + * @param payload Passed to fn as the first ath. + */ +extern int git_fs_path_walk_up( + git_str *pathbuf, + const char *ceiling, + int (*callback)(void *payload, const char *path), + void *payload); + + +enum { + GIT_FS_PATH_NOTEQUAL = 0, + GIT_FS_PATH_EQUAL = 1, + GIT_FS_PATH_PREFIX = 2 +}; + +/* + * Determines if a path is equal to or potentially a child of another. + * @param parent The possible parent + * @param child The possible child + */ +GIT_INLINE(int) git_fs_path_equal_or_prefixed( + const char *parent, + const char *child, + ssize_t *prefixlen) +{ + const char *p = parent, *c = child; + int lastslash = 0; + + while (*p && *c) { + lastslash = (*p == '/'); + + if (*p++ != *c++) + return GIT_FS_PATH_NOTEQUAL; + } + + if (*p != '\0') + return GIT_FS_PATH_NOTEQUAL; + + if (*c == '\0') { + if (prefixlen) + *prefixlen = p - parent; + + return GIT_FS_PATH_EQUAL; + } + + if (*c == '/' || lastslash) { + if (prefixlen) + *prefixlen = (p - parent) - lastslash; + + return GIT_FS_PATH_PREFIX; + } + + return GIT_FS_PATH_NOTEQUAL; +} + +/* translate errno to libgit2 error code and set error message */ +extern int git_fs_path_set_error( + int errno_value, const char *path, const char *action); + +/* check if non-ascii characters are present in filename */ +extern bool git_fs_path_has_non_ascii(const char *path, size_t pathlen); + +#define GIT_PATH_REPO_ENCODING "UTF-8" + +#ifdef __APPLE__ +#define GIT_PATH_NATIVE_ENCODING "UTF-8-MAC" +#else +#define GIT_PATH_NATIVE_ENCODING "UTF-8" +#endif + +#ifdef GIT_USE_ICONV + +#include + +typedef struct { + iconv_t map; + git_str buf; +} git_fs_path_iconv_t; + +#define GIT_PATH_ICONV_INIT { (iconv_t)-1, GIT_STR_INIT } + +/* Init iconv data for converting decomposed UTF-8 to precomposed */ +extern int git_fs_path_iconv_init_precompose(git_fs_path_iconv_t *ic); + +/* Clear allocated iconv data */ +extern void git_fs_path_iconv_clear(git_fs_path_iconv_t *ic); + +/* + * Rewrite `in` buffer using iconv map if necessary, replacing `in` + * pointer internal iconv buffer if rewrite happened. The `in` pointer + * will be left unchanged if no rewrite was needed. + */ +extern int git_fs_path_iconv(git_fs_path_iconv_t *ic, const char **in, size_t *inlen); + +#endif /* GIT_USE_ICONV */ + +extern bool git_fs_path_does_decompose_unicode(const char *root); + + +typedef struct git_fs_path_diriter git_fs_path_diriter; + +#if defined(GIT_WIN32) && !defined(__MINGW32__) + +struct git_fs_path_diriter +{ + git_win32_path path; + size_t parent_len; + + git_str path_utf8; + size_t parent_utf8_len; + + HANDLE handle; + + unsigned int flags; + + WIN32_FIND_DATAW current; + unsigned int needs_next; +}; + +#define GIT_FS_PATH_DIRITER_INIT { {0}, 0, GIT_STR_INIT, 0, INVALID_HANDLE_VALUE } + +#else + +struct git_fs_path_diriter +{ + git_str path; + size_t parent_len; + + unsigned int flags; + + DIR *dir; + +#ifdef GIT_USE_ICONV + git_fs_path_iconv_t ic; +#endif +}; + +#define GIT_FS_PATH_DIRITER_INIT { GIT_STR_INIT } + +#endif + +/** + * Initialize a directory iterator. + * + * @param diriter Pointer to a diriter structure that will be setup. + * @param path The path that will be iterated over + * @param flags Directory reader flags + * @return 0 or an error code + */ +extern int git_fs_path_diriter_init( + git_fs_path_diriter *diriter, + const char *path, + unsigned int flags); + +/** + * Advance the directory iterator. Will return GIT_ITEROVER when + * the iteration has completed successfully. + * + * @param diriter The directory iterator + * @return 0, GIT_ITEROVER, or an error code + */ +extern int git_fs_path_diriter_next(git_fs_path_diriter *diriter); + +/** + * Returns the file name of the current item in the iterator. + * + * @param out Pointer to store the path in + * @param out_len Pointer to store the length of the path in + * @param diriter The directory iterator + * @return 0 or an error code + */ +extern int git_fs_path_diriter_filename( + const char **out, + size_t *out_len, + git_fs_path_diriter *diriter); + +/** + * Returns the full path of the current item in the iterator; that + * is the current filename plus the path of the directory that the + * iterator was constructed with. + * + * @param out Pointer to store the path in + * @param out_len Pointer to store the length of the path in + * @param diriter The directory iterator + * @return 0 or an error code + */ +extern int git_fs_path_diriter_fullpath( + const char **out, + size_t *out_len, + git_fs_path_diriter *diriter); + +/** + * Performs an `lstat` on the current item in the iterator. + * + * @param out Pointer to store the stat data in + * @param diriter The directory iterator + * @return 0 or an error code + */ +extern int git_fs_path_diriter_stat(struct stat *out, git_fs_path_diriter *diriter); + +/** + * Closes the directory iterator. + * + * @param diriter The directory iterator + */ +extern void git_fs_path_diriter_free(git_fs_path_diriter *diriter); + +/** + * Load all directory entries (except '.' and '..') into a vector. + * + * For cases where `git_fs_path_direach()` is not appropriate, this + * allows you to load the filenames in a directory into a vector + * of strings. That vector can then be sorted, iterated, or whatever. + * Remember to free alloc of the allocated strings when you are done. + * + * @param contents Vector to fill with directory entry names. + * @param path The directory to read from. + * @param prefix_len When inserting entries, the trailing part of path + * will be prefixed after this length. I.e. given path "/a/b" and + * prefix_len 3, the entries will look like "b/e1", "b/e2", etc. + * @param flags Combination of GIT_FS_PATH_DIR flags. + */ +extern int git_fs_path_dirload( + git_vector *contents, + const char *path, + size_t prefix_len, + uint32_t flags); + + +/* Used for paths to repositories on the filesystem */ +extern bool git_fs_path_is_local_file_url(const char *file_url); +extern int git_fs_path_from_url_or_path(git_str *local_path_out, const char *url_or_path); + +/* Flags to determine path validity in `git_fs_path_isvalid` */ +#define GIT_FS_PATH_REJECT_EMPTY_COMPONENT (1 << 0) +#define GIT_FS_PATH_REJECT_TRAVERSAL (1 << 1) +#define GIT_FS_PATH_REJECT_SLASH (1 << 2) +#define GIT_FS_PATH_REJECT_BACKSLASH (1 << 3) +#define GIT_FS_PATH_REJECT_TRAILING_DOT (1 << 4) +#define GIT_FS_PATH_REJECT_TRAILING_SPACE (1 << 5) +#define GIT_FS_PATH_REJECT_TRAILING_COLON (1 << 6) +#define GIT_FS_PATH_REJECT_DOS_PATHS (1 << 7) +#define GIT_FS_PATH_REJECT_NT_CHARS (1 << 8) +#define GIT_FS_PATH_REJECT_LONG_PATHS (1 << 9) + +#define GIT_FS_PATH_REJECT_MAX (1 << 9) + +/* Default path safety for writing files to disk: since we use the + * Win32 "File Namespace" APIs ("\\?\") we need to protect from + * paths that the normal Win32 APIs would not write. + */ +#ifdef GIT_WIN32 +# define GIT_FS_PATH_REJECT_FILESYSTEM_DEFAULTS \ + GIT_FS_PATH_REJECT_EMPTY_COMPONENT | \ + GIT_FS_PATH_REJECT_TRAVERSAL | \ + GIT_FS_PATH_REJECT_BACKSLASH | \ + GIT_FS_PATH_REJECT_TRAILING_DOT | \ + GIT_FS_PATH_REJECT_TRAILING_SPACE | \ + GIT_FS_PATH_REJECT_TRAILING_COLON | \ + GIT_FS_PATH_REJECT_DOS_PATHS | \ + GIT_FS_PATH_REJECT_NT_CHARS +#else +# define GIT_FS_PATH_REJECT_FILESYSTEM_DEFAULTS \ + GIT_FS_PATH_REJECT_EMPTY_COMPONENT | \ + GIT_FS_PATH_REJECT_TRAVERSAL +#endif + +/** + * Validate a filesystem path; with custom callbacks per-character and + * per-path component. + */ +extern bool git_fs_path_str_is_valid_ext( + const git_str *path, + unsigned int flags, + bool (*validate_char_cb)(char ch, void *payload), + bool (*validate_component_cb)(const char *component, size_t len, void *payload), + bool (*validate_length_cb)(const char *component, size_t len, size_t utf8_char_len), + void *payload); + +GIT_INLINE(bool) git_fs_path_is_valid_ext( + const char *path, + unsigned int flags, + bool (*validate_char_cb)(char ch, void *payload), + bool (*validate_component_cb)(const char *component, size_t len, void *payload), + bool (*validate_length_cb)(const char *component, size_t len, size_t utf8_char_len), + void *payload) +{ + const git_str str = GIT_STR_INIT_CONST(path, SIZE_MAX); + return git_fs_path_str_is_valid_ext( + &str, + flags, + validate_char_cb, + validate_component_cb, + validate_length_cb, + payload); +} + +/** + * Validate a filesystem path. This ensures that the given path is legal + * and does not contain any "unsafe" components like path traversal ('.' + * or '..'), characters that are inappropriate for lesser filesystems + * (trailing ' ' or ':' characters), or filenames ("component names") + * that are not supported ('AUX', 'COM1"). + */ +GIT_INLINE(bool) git_fs_path_is_valid( + const char *path, + unsigned int flags) +{ + const git_str str = GIT_STR_INIT_CONST(path, SIZE_MAX); + return git_fs_path_str_is_valid_ext(&str, flags, NULL, NULL, NULL, NULL); +} + +/** Validate a filesystem path in a `git_str`. */ +GIT_INLINE(bool) git_fs_path_str_is_valid( + const git_str *path, + unsigned int flags) +{ + return git_fs_path_str_is_valid_ext(path, flags, NULL, NULL, NULL, NULL); +} + +extern int git_fs_path_validate_str_length_with_suffix( + git_str *path, + size_t suffix_len); + +/** + * Validate an on-disk path, taking into account that it will have a + * suffix appended (eg, `.lock`). + */ +GIT_INLINE(int) git_fs_path_validate_filesystem_with_suffix( + const char *path, + size_t path_len, + size_t suffix_len) +{ +#ifdef GIT_WIN32 + size_t path_chars, total_chars; + + path_chars = git_utf8_char_length(path, path_len); + + if (GIT_ADD_SIZET_OVERFLOW(&total_chars, path_chars, suffix_len) || + total_chars > MAX_PATH) { + git_error_set(GIT_ERROR_FILESYSTEM, "path too long: '%s'", path); + return -1; + } + return 0; +#else + GIT_UNUSED(path); + GIT_UNUSED(path_len); + GIT_UNUSED(suffix_len); + return 0; +#endif +} + +/** + * Validate an path on the filesystem. This ensures that the given + * path is valid for the operating system/platform; for example, this + * will ensure that the given absolute path is smaller than MAX_PATH on + * Windows. + * + * For paths within the working directory, you should use ensure that + * `core.longpaths` is obeyed. Use `git_fs_path_validate_workdir`. + */ +GIT_INLINE(int) git_fs_path_validate_filesystem( + const char *path, + size_t path_len) +{ + return git_fs_path_validate_filesystem_with_suffix(path, path_len, 0); +} + +/** + * Convert any backslashes into slashes + */ +int git_fs_path_normalize_slashes(git_str *out, const char *path); + +bool git_fs_path_supports_symlinks(const char *dir); + +typedef enum { + GIT_FS_PATH_OWNER_NONE = 0, + + /** The file must be owned by the current user. */ + GIT_FS_PATH_OWNER_CURRENT_USER = (1 << 0), + + /** The file must be owned by the system account. */ + GIT_FS_PATH_OWNER_ADMINISTRATOR = (1 << 1), + + /** + * The file may be owned by a system account if the current + * user is in an administrator group. Windows only; this is + * a noop on non-Windows systems. + */ + GIT_FS_PATH_USER_IS_ADMINISTRATOR = (1 << 2), + + /** + * The file is owned by the current user, who is running `sudo`. + */ + GIT_FS_PATH_OWNER_RUNNING_SUDO = (1 << 3), + + /** The file may be owned by another user. */ + GIT_FS_PATH_OWNER_OTHER = (1 << 4) +} git_fs_path_owner_t; + +/** + * Sets the mock ownership for files; subsequent calls to + * `git_fs_path_owner_is_*` functions will return this data until + * cleared with `GIT_FS_PATH_OWNER_NONE`. + */ +void git_fs_path__set_owner(git_fs_path_owner_t owner); + +/** Verify that the file in question is owned by the given owner. */ +int git_fs_path_owner_is( + bool *out, + const char *path, + git_fs_path_owner_t owner_type); + +/** + * Verify that the file in question is owned by an administrator or system + * account. + */ +int git_fs_path_owner_is_system(bool *out, const char *path); + +/** + * Verify that the file in question is owned by the current user; + */ + +int git_fs_path_owner_is_current_user(bool *out, const char *path); + +/** + * Search the current PATH for the given executable, returning the full + * path if it is found. + */ +int git_fs_path_find_executable(git_str *fullpath, const char *executable); + +#endif diff --git a/src/util/futils.c b/src/util/futils.c new file mode 100644 index 0000000..7b5a24b --- /dev/null +++ b/src/util/futils.c @@ -0,0 +1,1236 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "futils.h" + +#include "runtime.h" +#include "strmap.h" +#include "hash.h" +#include "rand.h" + +#include + +#define GIT_FILEMODE_DEFAULT 0100666 + +int git_futils_mkpath2file(const char *file_path, const mode_t mode) +{ + return git_futils_mkdir( + file_path, mode, + GIT_MKDIR_PATH | GIT_MKDIR_SKIP_LAST | GIT_MKDIR_VERIFY_DIR); +} + +int git_futils_mktmp(git_str *path_out, const char *filename, mode_t mode) +{ + const int open_flags = O_RDWR | O_CREAT | O_EXCL | O_BINARY | O_CLOEXEC; + unsigned int tries = 32; + int fd; + + while (tries--) { + uint64_t rand = git_rand_next(); + + git_str_sets(path_out, filename); + git_str_puts(path_out, "_git2_"); + git_str_encode_hexstr(path_out, (void *)&rand, sizeof(uint64_t)); + + if (git_str_oom(path_out)) + return -1; + + /* Note that we open with O_CREAT | O_EXCL */ + if ((fd = p_open(path_out->ptr, open_flags, mode)) >= 0) + return fd; + } + + git_error_set(GIT_ERROR_OS, + "failed to create temporary file '%s'", path_out->ptr); + git_str_dispose(path_out); + return -1; +} + +int git_futils_creat_withpath(const char *path, const mode_t dirmode, const mode_t mode) +{ + int fd; + + if (git_futils_mkpath2file(path, dirmode) < 0) + return -1; + + fd = p_creat(path, mode); + if (fd < 0) { + git_error_set(GIT_ERROR_OS, "failed to create file '%s'", path); + return -1; + } + + return fd; +} + +int git_futils_creat_locked(const char *path, const mode_t mode) +{ + int fd = p_open(path, O_WRONLY | O_CREAT | O_EXCL | O_BINARY | O_CLOEXEC, + mode); + + if (fd < 0) { + int error = errno; + git_error_set(GIT_ERROR_OS, "failed to create locked file '%s'", path); + switch (error) { + case EEXIST: + return GIT_ELOCKED; + case ENOENT: + return GIT_ENOTFOUND; + default: + return -1; + } + } + + return fd; +} + +int git_futils_creat_locked_withpath(const char *path, const mode_t dirmode, const mode_t mode) +{ + if (git_futils_mkpath2file(path, dirmode) < 0) + return -1; + + return git_futils_creat_locked(path, mode); +} + +int git_futils_open_ro(const char *path) +{ + int fd = p_open(path, O_RDONLY); + if (fd < 0) + return git_fs_path_set_error(errno, path, "open"); + return fd; +} + +int git_futils_truncate(const char *path, int mode) +{ + int fd = p_open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, mode); + if (fd < 0) + return git_fs_path_set_error(errno, path, "open"); + + close(fd); + return 0; +} + +int git_futils_filesize(uint64_t *out, git_file fd) +{ + struct stat sb; + + if (p_fstat(fd, &sb)) { + git_error_set(GIT_ERROR_OS, "failed to stat file descriptor"); + return -1; + } + + if (sb.st_size < 0) { + git_error_set(GIT_ERROR_INVALID, "invalid file size"); + return -1; + } + + *out = sb.st_size; + return 0; +} + +mode_t git_futils_canonical_mode(mode_t raw_mode) +{ + if (S_ISREG(raw_mode)) + return S_IFREG | GIT_PERMS_CANONICAL(raw_mode); + else if (S_ISLNK(raw_mode)) + return S_IFLNK; + else if (S_ISGITLINK(raw_mode)) + return S_IFGITLINK; + else if (S_ISDIR(raw_mode)) + return S_IFDIR; + else + return 0; +} + +int git_futils_readbuffer_fd(git_str *buf, git_file fd, size_t len) +{ + ssize_t read_size = 0; + size_t alloc_len; + + git_str_clear(buf); + + if (!git__is_ssizet(len)) { + git_error_set(GIT_ERROR_INVALID, "read too large"); + return -1; + } + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, len, 1); + if (git_str_grow(buf, alloc_len) < 0) + return -1; + + /* p_read loops internally to read len bytes */ + read_size = p_read(fd, buf->ptr, len); + + if (read_size < 0) { + git_error_set(GIT_ERROR_OS, "failed to read descriptor"); + git_str_dispose(buf); + return -1; + } + + if ((size_t)read_size != len) { + git_error_set(GIT_ERROR_FILESYSTEM, "could not read (expected %" PRIuZ " bytes, read %" PRIuZ ")", len, (size_t)read_size); + git_str_dispose(buf); + return -1; + } + + buf->ptr[read_size] = '\0'; + buf->size = read_size; + + return 0; +} + +int git_futils_readbuffer_fd_full(git_str *buf, git_file fd) +{ + static size_t blocksize = 10240; + size_t alloc_len = 0, total_size = 0; + ssize_t read_size = 0; + + git_str_clear(buf); + + while (true) { + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, blocksize); + + if (git_str_grow(buf, alloc_len) < 0) + return -1; + + /* p_read loops internally to read blocksize bytes */ + read_size = p_read(fd, buf->ptr, blocksize); + + if (read_size < 0) { + git_error_set(GIT_ERROR_OS, "failed to read descriptor"); + git_str_dispose(buf); + return -1; + } + + total_size += read_size; + + if ((size_t)read_size < blocksize) { + break; + } + } + + buf->ptr[total_size] = '\0'; + buf->size = total_size; + + return 0; +} + +int git_futils_readbuffer_updated( + git_str *out, + const char *path, + unsigned char checksum[GIT_HASH_SHA256_SIZE], + int *updated) +{ + int error; + git_file fd; + struct stat st; + git_str buf = GIT_STR_INIT; + unsigned char checksum_new[GIT_HASH_SHA256_SIZE]; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(path && *path); + + if (updated != NULL) + *updated = 0; + + if (p_stat(path, &st) < 0) + return git_fs_path_set_error(errno, path, "stat"); + + + if (S_ISDIR(st.st_mode)) { + git_error_set(GIT_ERROR_INVALID, "requested file is a directory"); + return GIT_ENOTFOUND; + } + + if (!git__is_sizet(st.st_size+1)) { + git_error_set(GIT_ERROR_OS, "invalid regular file stat for '%s'", path); + return -1; + } + + if ((fd = git_futils_open_ro(path)) < 0) + return fd; + + if (git_futils_readbuffer_fd(&buf, fd, (size_t)st.st_size) < 0) { + p_close(fd); + return -1; + } + + p_close(fd); + + if (checksum) { + error = git_hash_buf(checksum_new, buf.ptr, + buf.size, GIT_HASH_ALGORITHM_SHA256); + + if (error < 0) { + git_str_dispose(&buf); + return error; + } + + /* + * If we were given a checksum, we only want to use it if it's different + */ + if (!memcmp(checksum, checksum_new, GIT_HASH_SHA256_SIZE)) { + git_str_dispose(&buf); + if (updated) + *updated = 0; + + return 0; + } + + memcpy(checksum, checksum_new, GIT_HASH_SHA256_SIZE); + } + + /* + * If we're here, the file did change, or the user didn't have an old version + */ + if (updated != NULL) + *updated = 1; + + git_str_swap(out, &buf); + git_str_dispose(&buf); + + return 0; +} + +int git_futils_readbuffer(git_str *buf, const char *path) +{ + return git_futils_readbuffer_updated(buf, path, NULL, NULL); +} + +int git_futils_writebuffer( + const git_str *buf, const char *path, int flags, mode_t mode) +{ + int fd, do_fsync = 0, error = 0; + + if (!flags) + flags = O_CREAT | O_TRUNC | O_WRONLY; + + if ((flags & O_FSYNC) != 0) + do_fsync = 1; + + flags &= ~O_FSYNC; + + if (!mode) + mode = GIT_FILEMODE_DEFAULT; + + if ((fd = p_open(path, flags, mode)) < 0) { + git_error_set(GIT_ERROR_OS, "could not open '%s' for writing", path); + return fd; + } + + if ((error = p_write(fd, git_str_cstr(buf), git_str_len(buf))) < 0) { + git_error_set(GIT_ERROR_OS, "could not write to '%s'", path); + (void)p_close(fd); + return error; + } + + if (do_fsync && (error = p_fsync(fd)) < 0) { + git_error_set(GIT_ERROR_OS, "could not fsync '%s'", path); + p_close(fd); + return error; + } + + if ((error = p_close(fd)) < 0) { + git_error_set(GIT_ERROR_OS, "error while closing '%s'", path); + return error; + } + + if (do_fsync && (flags & O_CREAT)) + error = git_futils_fsync_parent(path); + + return error; +} + +int git_futils_mv_withpath(const char *from, const char *to, const mode_t dirmode) +{ + if (git_futils_mkpath2file(to, dirmode) < 0) + return -1; + + if (p_rename(from, to) < 0) { + git_error_set(GIT_ERROR_OS, "failed to rename '%s' to '%s'", from, to); + return -1; + } + + return 0; +} + +int git_futils_mmap_ro(git_map *out, git_file fd, off64_t begin, size_t len) +{ + return p_mmap(out, len, GIT_PROT_READ, GIT_MAP_SHARED, fd, begin); +} + +int git_futils_mmap_ro_file(git_map *out, const char *path) +{ + git_file fd = git_futils_open_ro(path); + uint64_t len; + int result; + + if (fd < 0) + return fd; + + if ((result = git_futils_filesize(&len, fd)) < 0) + goto out; + + if (!git__is_sizet(len)) { + git_error_set(GIT_ERROR_OS, "file `%s` too large to mmap", path); + result = -1; + goto out; + } + + result = git_futils_mmap_ro(out, fd, 0, (size_t)len); +out: + p_close(fd); + return result; +} + +void git_futils_mmap_free(git_map *out) +{ + p_munmap(out); +} + +GIT_INLINE(int) mkdir_validate_dir( + const char *path, + struct stat *st, + mode_t mode, + uint32_t flags, + struct git_futils_mkdir_options *opts) +{ + /* with exclusive create, existing dir is an error */ + if ((flags & GIT_MKDIR_EXCL) != 0) { + git_error_set(GIT_ERROR_FILESYSTEM, + "failed to make directory '%s': directory exists", path); + return GIT_EEXISTS; + } + + if ((S_ISREG(st->st_mode) && (flags & GIT_MKDIR_REMOVE_FILES)) || + (S_ISLNK(st->st_mode) && (flags & GIT_MKDIR_REMOVE_SYMLINKS))) { + if (p_unlink(path) < 0) { + git_error_set(GIT_ERROR_OS, "failed to remove %s '%s'", + S_ISLNK(st->st_mode) ? "symlink" : "file", path); + return GIT_EEXISTS; + } + + opts->perfdata.mkdir_calls++; + + if (p_mkdir(path, mode) < 0) { + git_error_set(GIT_ERROR_OS, "failed to make directory '%s'", path); + return GIT_EEXISTS; + } + } + + else if (S_ISLNK(st->st_mode)) { + /* Re-stat the target, make sure it's a directory */ + opts->perfdata.stat_calls++; + + if (p_stat(path, st) < 0) { + git_error_set(GIT_ERROR_OS, "failed to make directory '%s'", path); + return GIT_EEXISTS; + } + } + + else if (!S_ISDIR(st->st_mode)) { + git_error_set(GIT_ERROR_FILESYSTEM, + "failed to make directory '%s': directory exists", path); + return GIT_EEXISTS; + } + + return 0; +} + +GIT_INLINE(int) mkdir_validate_mode( + const char *path, + struct stat *st, + bool terminal_path, + mode_t mode, + uint32_t flags, + struct git_futils_mkdir_options *opts) +{ + if (((terminal_path && (flags & GIT_MKDIR_CHMOD) != 0) || + (flags & GIT_MKDIR_CHMOD_PATH) != 0) && st->st_mode != mode) { + + opts->perfdata.chmod_calls++; + + if (p_chmod(path, mode) < 0) { + git_error_set(GIT_ERROR_OS, "failed to set permissions on '%s'", path); + return -1; + } + } + + return 0; +} + +GIT_INLINE(int) mkdir_canonicalize( + git_str *path, + uint32_t flags) +{ + ssize_t root_len; + + if (path->size == 0) { + git_error_set(GIT_ERROR_OS, "attempt to create empty path"); + return -1; + } + + /* Trim trailing slashes (except the root) */ + if ((root_len = git_fs_path_root(path->ptr)) < 0) + root_len = 0; + else + root_len++; + + while (path->size > (size_t)root_len && path->ptr[path->size - 1] == '/') + path->ptr[--path->size] = '\0'; + + /* if we are not supposed to made the last element, truncate it */ + if ((flags & GIT_MKDIR_SKIP_LAST2) != 0) { + git_fs_path_dirname_r(path, path->ptr); + flags |= GIT_MKDIR_SKIP_LAST; + } + if ((flags & GIT_MKDIR_SKIP_LAST) != 0) { + git_fs_path_dirname_r(path, path->ptr); + } + + /* We were either given the root path (or trimmed it to + * the root), we don't have anything to do. + */ + if (path->size <= (size_t)root_len) + git_str_clear(path); + + return 0; +} + +int git_futils_mkdir( + const char *path, + mode_t mode, + uint32_t flags) +{ + git_str make_path = GIT_STR_INIT, parent_path = GIT_STR_INIT; + const char *relative; + struct git_futils_mkdir_options opts = { 0 }; + struct stat st; + size_t depth = 0; + int len = 0, root_len, error; + + if ((error = git_str_puts(&make_path, path)) < 0 || + (error = mkdir_canonicalize(&make_path, flags)) < 0 || + (error = git_str_puts(&parent_path, make_path.ptr)) < 0 || + make_path.size == 0) + goto done; + + root_len = git_fs_path_root(make_path.ptr); + + /* find the first parent directory that exists. this will be used + * as the base to dirname_relative. + */ + for (relative = make_path.ptr; parent_path.size; ) { + error = p_lstat(parent_path.ptr, &st); + + if (error == 0) { + break; + } else if (errno != ENOENT) { + git_error_set(GIT_ERROR_OS, "failed to stat '%s'", parent_path.ptr); + error = -1; + goto done; + } + + depth++; + + /* examine the parent of the current path */ + if ((len = git_fs_path_dirname_r(&parent_path, parent_path.ptr)) < 0) { + error = len; + goto done; + } + + GIT_ASSERT(len); + + /* + * We've walked all the given path's parents and it's either relative + * (the parent is simply '.') or rooted (the length is less than or + * equal to length of the root path). The path may be less than the + * root path length on Windows, where `C:` == `C:/`. + */ + if ((len == 1 && parent_path.ptr[0] == '.') || + (len == 1 && parent_path.ptr[0] == '/') || + len <= root_len) { + relative = make_path.ptr; + break; + } + + relative = make_path.ptr + len + 1; + + /* not recursive? just make this directory relative to its parent. */ + if ((flags & GIT_MKDIR_PATH) == 0) + break; + } + + /* we found an item at the location we're trying to create, + * validate it. + */ + if (depth == 0) { + error = mkdir_validate_dir(make_path.ptr, &st, mode, flags, &opts); + + if (!error) + error = mkdir_validate_mode( + make_path.ptr, &st, true, mode, flags, &opts); + + goto done; + } + + /* we already took `SKIP_LAST` and `SKIP_LAST2` into account when + * canonicalizing `make_path`. + */ + flags &= ~(GIT_MKDIR_SKIP_LAST2 | GIT_MKDIR_SKIP_LAST); + + error = git_futils_mkdir_relative(relative, + parent_path.size ? parent_path.ptr : NULL, mode, flags, &opts); + +done: + git_str_dispose(&make_path); + git_str_dispose(&parent_path); + return error; +} + +int git_futils_mkdir_r(const char *path, const mode_t mode) +{ + return git_futils_mkdir(path, mode, GIT_MKDIR_PATH); +} + +int git_futils_mkdir_relative( + const char *relative_path, + const char *base, + mode_t mode, + uint32_t flags, + struct git_futils_mkdir_options *opts) +{ + git_str make_path = GIT_STR_INIT; + ssize_t root = 0, min_root_len; + char lastch = '/', *tail; + struct stat st; + struct git_futils_mkdir_options empty_opts = {0}; + int error; + + if (!opts) + opts = &empty_opts; + + /* build path and find "root" where we should start calling mkdir */ + if (git_fs_path_join_unrooted(&make_path, relative_path, base, &root) < 0) + return -1; + + if ((error = mkdir_canonicalize(&make_path, flags)) < 0 || + make_path.size == 0) + goto done; + + /* if we are not supposed to make the whole path, reset root */ + if ((flags & GIT_MKDIR_PATH) == 0) + root = git_str_rfind(&make_path, '/'); + + /* advance root past drive name or network mount prefix */ + min_root_len = git_fs_path_root(make_path.ptr); + if (root < min_root_len) + root = min_root_len; + while (root >= 0 && make_path.ptr[root] == '/') + ++root; + + /* clip root to make_path length */ + if (root > (ssize_t)make_path.size) + root = (ssize_t)make_path.size; /* i.e. NUL byte of string */ + if (root < 0) + root = 0; + + /* walk down tail of path making each directory */ + for (tail = &make_path.ptr[root]; *tail; *tail = lastch) { + bool mkdir_attempted = false; + + /* advance tail to include next path component */ + while (*tail == '/') + tail++; + while (*tail && *tail != '/') + tail++; + + /* truncate path at next component */ + lastch = *tail; + *tail = '\0'; + st.st_mode = 0; + + if (opts->dir_map && git_strmap_exists(opts->dir_map, make_path.ptr)) + continue; + + /* See what's going on with this path component */ + opts->perfdata.stat_calls++; + +retry_lstat: + if (p_lstat(make_path.ptr, &st) < 0) { + if (mkdir_attempted || errno != ENOENT) { + git_error_set(GIT_ERROR_OS, "cannot access component in path '%s'", make_path.ptr); + error = -1; + goto done; + } + + git_error_clear(); + opts->perfdata.mkdir_calls++; + mkdir_attempted = true; + if (p_mkdir(make_path.ptr, mode) < 0) { + if (errno == EEXIST) + goto retry_lstat; + git_error_set(GIT_ERROR_OS, "failed to make directory '%s'", make_path.ptr); + error = -1; + goto done; + } + } else { + if ((error = mkdir_validate_dir( + make_path.ptr, &st, mode, flags, opts)) < 0) + goto done; + } + + /* chmod if requested and necessary */ + if ((error = mkdir_validate_mode( + make_path.ptr, &st, (lastch == '\0'), mode, flags, opts)) < 0) + goto done; + + if (opts->dir_map && opts->pool) { + char *cache_path; + size_t alloc_size; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_size, make_path.size, 1); + cache_path = git_pool_malloc(opts->pool, alloc_size); + GIT_ERROR_CHECK_ALLOC(cache_path); + + memcpy(cache_path, make_path.ptr, make_path.size + 1); + + if ((error = git_strmap_set(opts->dir_map, cache_path, cache_path)) < 0) + goto done; + } + } + + error = 0; + + /* check that full path really is a directory if requested & needed */ + if ((flags & GIT_MKDIR_VERIFY_DIR) != 0 && + lastch != '\0') { + opts->perfdata.stat_calls++; + + if (p_stat(make_path.ptr, &st) < 0 || !S_ISDIR(st.st_mode)) { + git_error_set(GIT_ERROR_OS, "path is not a directory '%s'", + make_path.ptr); + error = GIT_ENOTFOUND; + } + } + +done: + git_str_dispose(&make_path); + return error; +} + +typedef struct { + const char *base; + size_t baselen; + uint32_t flags; + int depth; +} futils__rmdir_data; + +#define FUTILS_MAX_DEPTH 100 + +static int futils__error_cannot_rmdir(const char *path, const char *filemsg) +{ + if (filemsg) + git_error_set(GIT_ERROR_OS, "could not remove directory '%s': %s", + path, filemsg); + else + git_error_set(GIT_ERROR_OS, "could not remove directory '%s'", path); + + return -1; +} + +static int futils__rm_first_parent(git_str *path, const char *ceiling) +{ + int error = GIT_ENOTFOUND; + struct stat st; + + while (error == GIT_ENOTFOUND) { + git_str_rtruncate_at_char(path, '/'); + + if (!path->size || git__prefixcmp(path->ptr, ceiling) != 0) + error = 0; + else if (p_lstat_posixly(path->ptr, &st) == 0) { + if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) + error = p_unlink(path->ptr); + else if (!S_ISDIR(st.st_mode)) + error = -1; /* fail to remove non-regular file */ + } else if (errno != ENOTDIR) + error = -1; + } + + if (error) + futils__error_cannot_rmdir(path->ptr, "cannot remove parent"); + + return error; +} + +static int futils__rmdir_recurs_foreach(void *opaque, git_str *path) +{ + int error = 0; + futils__rmdir_data *data = opaque; + struct stat st; + + if (data->depth > FUTILS_MAX_DEPTH) + error = futils__error_cannot_rmdir( + path->ptr, "directory nesting too deep"); + + else if ((error = p_lstat_posixly(path->ptr, &st)) < 0) { + if (errno == ENOENT) + error = 0; + else if (errno == ENOTDIR) { + /* asked to remove a/b/c/d/e and a/b is a normal file */ + if ((data->flags & GIT_RMDIR_REMOVE_BLOCKERS) != 0) + error = futils__rm_first_parent(path, data->base); + else + futils__error_cannot_rmdir( + path->ptr, "parent is not directory"); + } + else + error = git_fs_path_set_error(errno, path->ptr, "rmdir"); + } + + else if (S_ISDIR(st.st_mode)) { + data->depth++; + + error = git_fs_path_direach(path, 0, futils__rmdir_recurs_foreach, data); + + data->depth--; + + if (error < 0) + return error; + + if (data->depth == 0 && (data->flags & GIT_RMDIR_SKIP_ROOT) != 0) + return error; + + if ((error = p_rmdir(path->ptr)) < 0) { + if ((data->flags & GIT_RMDIR_SKIP_NONEMPTY) != 0 && + (errno == ENOTEMPTY || errno == EEXIST || errno == EBUSY)) + error = 0; + else + error = git_fs_path_set_error(errno, path->ptr, "rmdir"); + } + } + + else if ((data->flags & GIT_RMDIR_REMOVE_FILES) != 0) { + if (p_unlink(path->ptr) < 0) + error = git_fs_path_set_error(errno, path->ptr, "remove"); + } + + else if ((data->flags & GIT_RMDIR_SKIP_NONEMPTY) == 0) + error = futils__error_cannot_rmdir(path->ptr, "still present"); + + return error; +} + +static int futils__rmdir_empty_parent(void *opaque, const char *path) +{ + futils__rmdir_data *data = opaque; + int error = 0; + + if (strlen(path) <= data->baselen) + error = GIT_ITEROVER; + + else if (p_rmdir(path) < 0) { + int en = errno; + + if (en == ENOENT || en == ENOTDIR) { + /* do nothing */ + } else if ((data->flags & GIT_RMDIR_SKIP_NONEMPTY) == 0 && + en == EBUSY) { + error = git_fs_path_set_error(errno, path, "rmdir"); + } else if (en == ENOTEMPTY || en == EEXIST || en == EBUSY) { + error = GIT_ITEROVER; + } else { + error = git_fs_path_set_error(errno, path, "rmdir"); + } + } + + return error; +} + +int git_futils_rmdir_r( + const char *path, const char *base, uint32_t flags) +{ + int error; + git_str fullpath = GIT_STR_INIT; + futils__rmdir_data data; + + /* build path and find "root" where we should start calling mkdir */ + if (git_fs_path_join_unrooted(&fullpath, path, base, NULL) < 0) + return -1; + + memset(&data, 0, sizeof(data)); + data.base = base ? base : ""; + data.baselen = base ? strlen(base) : 0; + data.flags = flags; + + error = futils__rmdir_recurs_foreach(&data, &fullpath); + + /* remove now-empty parents if requested */ + if (!error && (flags & GIT_RMDIR_EMPTY_PARENTS) != 0) + error = git_fs_path_walk_up( + &fullpath, base, futils__rmdir_empty_parent, &data); + + if (error == GIT_ITEROVER) { + git_error_clear(); + error = 0; + } + + git_str_dispose(&fullpath); + + return error; +} + +int git_futils_fake_symlink(const char *target, const char *path) +{ + int retcode = GIT_ERROR; + int fd = git_futils_creat_withpath(path, 0755, 0644); + if (fd >= 0) { + retcode = p_write(fd, target, strlen(target)); + p_close(fd); + } + return retcode; +} + +static int cp_by_fd(int ifd, int ofd, bool close_fd_when_done) +{ + int error = 0; + char buffer[GIT_BUFSIZE_FILEIO]; + ssize_t len = 0; + + while (!error && (len = p_read(ifd, buffer, sizeof(buffer))) > 0) + /* p_write() does not have the same semantics as write(). It loops + * internally and will return 0 when it has completed writing. + */ + error = p_write(ofd, buffer, len); + + if (len < 0) { + git_error_set(GIT_ERROR_OS, "read error while copying file"); + error = (int)len; + } + + if (error < 0) + git_error_set(GIT_ERROR_OS, "write error while copying file"); + + if (close_fd_when_done) { + p_close(ifd); + p_close(ofd); + } + + return error; +} + +int git_futils_cp(const char *from, const char *to, mode_t filemode) +{ + int ifd, ofd; + + if ((ifd = git_futils_open_ro(from)) < 0) + return ifd; + + if ((ofd = p_open(to, O_WRONLY | O_CREAT | O_EXCL, filemode)) < 0) { + p_close(ifd); + return git_fs_path_set_error(errno, to, "open for writing"); + } + + return cp_by_fd(ifd, ofd, true); +} + +int git_futils_touch(const char *path, time_t *when) +{ + struct p_timeval times[2]; + int ret; + + times[0].tv_sec = times[1].tv_sec = when ? *when : time(NULL); + times[0].tv_usec = times[1].tv_usec = 0; + + ret = p_utimes(path, times); + + return (ret < 0) ? git_fs_path_set_error(errno, path, "touch") : 0; +} + +static int cp_link(const char *from, const char *to, size_t link_size) +{ + int error = 0; + ssize_t read_len; + char *link_data; + size_t alloc_size; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_size, link_size, 1); + link_data = git__malloc(alloc_size); + GIT_ERROR_CHECK_ALLOC(link_data); + + read_len = p_readlink(from, link_data, link_size); + if (read_len != (ssize_t)link_size) { + git_error_set(GIT_ERROR_OS, "failed to read symlink data for '%s'", from); + error = -1; + } + else { + link_data[read_len] = '\0'; + + if (p_symlink(link_data, to) < 0) { + git_error_set(GIT_ERROR_OS, "could not symlink '%s' as '%s'", + link_data, to); + error = -1; + } + } + + git__free(link_data); + return error; +} + +typedef struct { + const char *to_root; + git_str to; + ssize_t from_prefix; + uint32_t flags; + uint32_t mkdir_flags; + mode_t dirmode; +} cp_r_info; + +#define GIT_CPDIR__MKDIR_DONE_FOR_TO_ROOT (1u << 10) + +static int _cp_r_mkdir(cp_r_info *info, git_str *from) +{ + int error = 0; + + /* create root directory the first time we need to create a directory */ + if ((info->flags & GIT_CPDIR__MKDIR_DONE_FOR_TO_ROOT) == 0) { + error = git_futils_mkdir( + info->to_root, info->dirmode, + (info->flags & GIT_CPDIR_CHMOD_DIRS) ? GIT_MKDIR_CHMOD : 0); + + info->flags |= GIT_CPDIR__MKDIR_DONE_FOR_TO_ROOT; + } + + /* create directory with root as base to prevent excess chmods */ + if (!error) + error = git_futils_mkdir_relative( + from->ptr + info->from_prefix, info->to_root, + info->dirmode, info->mkdir_flags, NULL); + + return error; +} + +static int _cp_r_callback(void *ref, git_str *from) +{ + int error = 0; + cp_r_info *info = ref; + struct stat from_st, to_st; + bool exists = false; + + if ((info->flags & GIT_CPDIR_COPY_DOTFILES) == 0 && + from->ptr[git_fs_path_basename_offset(from)] == '.') + return 0; + + if ((error = git_str_joinpath( + &info->to, info->to_root, from->ptr + info->from_prefix)) < 0) + return error; + + if (!(error = git_fs_path_lstat(info->to.ptr, &to_st))) + exists = true; + else if (error != GIT_ENOTFOUND) + return error; + else { + git_error_clear(); + error = 0; + } + + if ((error = git_fs_path_lstat(from->ptr, &from_st)) < 0) + return error; + + if (S_ISDIR(from_st.st_mode)) { + mode_t oldmode = info->dirmode; + + /* if we are not chmod'ing, then overwrite dirmode */ + if ((info->flags & GIT_CPDIR_CHMOD_DIRS) == 0) + info->dirmode = from_st.st_mode; + + /* make directory now if CREATE_EMPTY_DIRS is requested and needed */ + if (!exists && (info->flags & GIT_CPDIR_CREATE_EMPTY_DIRS) != 0) + error = _cp_r_mkdir(info, from); + + /* recurse onto target directory */ + if (!error && (!exists || S_ISDIR(to_st.st_mode))) + error = git_fs_path_direach(from, 0, _cp_r_callback, info); + + if (oldmode != 0) + info->dirmode = oldmode; + + return error; + } + + if (exists) { + if ((info->flags & GIT_CPDIR_OVERWRITE) == 0) + return 0; + + if (p_unlink(info->to.ptr) < 0) { + git_error_set(GIT_ERROR_OS, "cannot overwrite existing file '%s'", + info->to.ptr); + return GIT_EEXISTS; + } + } + + /* Done if this isn't a regular file or a symlink */ + if (!S_ISREG(from_st.st_mode) && + (!S_ISLNK(from_st.st_mode) || + (info->flags & GIT_CPDIR_COPY_SYMLINKS) == 0)) + return 0; + + /* Make container directory on demand if needed */ + if ((info->flags & GIT_CPDIR_CREATE_EMPTY_DIRS) == 0 && + (error = _cp_r_mkdir(info, from)) < 0) + return error; + + /* make symlink or regular file */ + if (info->flags & GIT_CPDIR_LINK_FILES) { + if ((error = p_link(from->ptr, info->to.ptr)) < 0) + git_error_set(GIT_ERROR_OS, "failed to link '%s'", from->ptr); + } else if (S_ISLNK(from_st.st_mode)) { + error = cp_link(from->ptr, info->to.ptr, (size_t)from_st.st_size); + } else { + mode_t usemode = from_st.st_mode; + + if ((info->flags & GIT_CPDIR_SIMPLE_TO_MODE) != 0) + usemode = GIT_PERMS_FOR_WRITE(usemode); + + error = git_futils_cp(from->ptr, info->to.ptr, usemode); + } + + return error; +} + +int git_futils_cp_r( + const char *from, + const char *to, + uint32_t flags, + mode_t dirmode) +{ + int error; + git_str path = GIT_STR_INIT; + cp_r_info info; + + if (git_str_joinpath(&path, from, "") < 0) /* ensure trailing slash */ + return -1; + + memset(&info, 0, sizeof(info)); + info.to_root = to; + info.flags = flags; + info.dirmode = dirmode; + info.from_prefix = path.size; + git_str_init(&info.to, 0); + + /* precalculate mkdir flags */ + if ((flags & GIT_CPDIR_CREATE_EMPTY_DIRS) == 0) { + /* if not creating empty dirs, then use mkdir to create the path on + * demand right before files are copied. + */ + info.mkdir_flags = GIT_MKDIR_PATH | GIT_MKDIR_SKIP_LAST; + if ((flags & GIT_CPDIR_CHMOD_DIRS) != 0) + info.mkdir_flags |= GIT_MKDIR_CHMOD_PATH; + } else { + /* otherwise, we will do simple mkdir as directories are encountered */ + info.mkdir_flags = + ((flags & GIT_CPDIR_CHMOD_DIRS) != 0) ? GIT_MKDIR_CHMOD : 0; + } + + error = _cp_r_callback(&info, &path); + + git_str_dispose(&path); + git_str_dispose(&info.to); + + return error; +} + +int git_futils_filestamp_check( + git_futils_filestamp *stamp, const char *path) +{ + struct stat st; + + /* if the stamp is NULL, then always reload */ + if (stamp == NULL) + return 1; + + if (p_stat(path, &st) < 0) + return GIT_ENOTFOUND; + + if (stamp->mtime.tv_sec == st.st_mtime && +#if defined(GIT_USE_NSEC) + stamp->mtime.tv_nsec == st.st_mtime_nsec && +#endif + stamp->size == (uint64_t)st.st_size && + stamp->ino == (unsigned int)st.st_ino) + return 0; + + stamp->mtime.tv_sec = st.st_mtime; +#if defined(GIT_USE_NSEC) + stamp->mtime.tv_nsec = st.st_mtime_nsec; +#endif + stamp->size = (uint64_t)st.st_size; + stamp->ino = (unsigned int)st.st_ino; + + return 1; +} + +void git_futils_filestamp_set( + git_futils_filestamp *target, const git_futils_filestamp *source) +{ + if (source) + memcpy(target, source, sizeof(*target)); + else + memset(target, 0, sizeof(*target)); +} + + +void git_futils_filestamp_set_from_stat( + git_futils_filestamp *stamp, struct stat *st) +{ + if (st) { + stamp->mtime.tv_sec = st->st_mtime; +#if defined(GIT_USE_NSEC) + stamp->mtime.tv_nsec = st->st_mtime_nsec; +#else + stamp->mtime.tv_nsec = 0; +#endif + stamp->size = (uint64_t)st->st_size; + stamp->ino = (unsigned int)st->st_ino; + } else { + memset(stamp, 0, sizeof(*stamp)); + } +} + +int git_futils_fsync_dir(const char *path) +{ +#ifdef GIT_WIN32 + GIT_UNUSED(path); + return 0; +#else + int fd, error = -1; + + if ((fd = p_open(path, O_RDONLY)) < 0) { + git_error_set(GIT_ERROR_OS, "failed to open directory '%s' for fsync", path); + return -1; + } + + if ((error = p_fsync(fd)) < 0) + git_error_set(GIT_ERROR_OS, "failed to fsync directory '%s'", path); + + p_close(fd); + return error; +#endif +} + +int git_futils_fsync_parent(const char *path) +{ + char *parent; + int error; + + if ((parent = git_fs_path_dirname(path)) == NULL) + return -1; + + error = git_futils_fsync_dir(parent); + git__free(parent); + return error; +} diff --git a/src/util/futils.h b/src/util/futils.h new file mode 100644 index 0000000..3f207af --- /dev/null +++ b/src/util/futils.h @@ -0,0 +1,403 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_futils_h__ +#define INCLUDE_futils_h__ + +#include "git2_util.h" + +#include "map.h" +#include "posix.h" +#include "fs_path.h" +#include "pool.h" +#include "strmap.h" +#include "hash.h" + +/** + * Filebuffer methods + * + * Read whole files into an in-memory buffer for processing + */ +extern int git_futils_readbuffer(git_str *obj, const char *path); +extern int git_futils_readbuffer_updated( + git_str *obj, + const char *path, + unsigned char checksum[GIT_HASH_SHA1_SIZE], + int *updated); +extern int git_futils_readbuffer_fd_full(git_str *obj, git_file fd); +extern int git_futils_readbuffer_fd(git_str *obj, git_file fd, size_t len); + +/* Additional constants for `git_futils_writebuffer`'s `open_flags`. We + * support these internally and they will be removed before the `open` call. + */ +#ifndef O_FSYNC +# define O_FSYNC (1 << 31) +#endif + +extern int git_futils_writebuffer( + const git_str *buf, const char *path, int open_flags, mode_t mode); + +/** + * File utils + * + * These are custom filesystem-related helper methods. They are + * rather high level, and wrap the underlying POSIX methods + * + * All these methods return 0 on success, + * or an error code on failure and an error message is set. + */ + +/** + * Create and open a file, while also + * creating all the folders in its path + */ +extern int git_futils_creat_withpath(const char *path, const mode_t dirmode, const mode_t mode); + +/** + * Create and open a process-locked file + */ +extern int git_futils_creat_locked(const char *path, const mode_t mode); + +/** + * Create and open a process-locked file, while + * also creating all the folders in its path + */ +extern int git_futils_creat_locked_withpath(const char *path, const mode_t dirmode, const mode_t mode); + +/** + * Create a path recursively. + */ +extern int git_futils_mkdir_r(const char *path, const mode_t mode); + +/** + * Flags to pass to `git_futils_mkdir`. + * + * * GIT_MKDIR_EXCL is "exclusive" - i.e. generate an error if dir exists. + * * GIT_MKDIR_PATH says to make all components in the path. + * * GIT_MKDIR_CHMOD says to chmod the final directory entry after creation + * * GIT_MKDIR_CHMOD_PATH says to chmod each directory component in the path + * * GIT_MKDIR_SKIP_LAST says to leave off the last element of the path + * * GIT_MKDIR_SKIP_LAST2 says to leave off the last 2 elements of the path + * * GIT_MKDIR_VERIFY_DIR says confirm final item is a dir, not just EEXIST + * * GIT_MKDIR_REMOVE_FILES says to remove files and recreate dirs + * * GIT_MKDIR_REMOVE_SYMLINKS says to remove symlinks and recreate dirs + * + * Note that the chmod options will be executed even if the directory already + * exists, unless GIT_MKDIR_EXCL is given. + */ +typedef enum { + GIT_MKDIR_EXCL = 1, + GIT_MKDIR_PATH = 2, + GIT_MKDIR_CHMOD = 4, + GIT_MKDIR_CHMOD_PATH = 8, + GIT_MKDIR_SKIP_LAST = 16, + GIT_MKDIR_SKIP_LAST2 = 32, + GIT_MKDIR_VERIFY_DIR = 64, + GIT_MKDIR_REMOVE_FILES = 128, + GIT_MKDIR_REMOVE_SYMLINKS = 256 +} git_futils_mkdir_flags; + +struct git_futils_mkdir_perfdata +{ + size_t stat_calls; + size_t mkdir_calls; + size_t chmod_calls; +}; + +struct git_futils_mkdir_options +{ + git_strmap *dir_map; + git_pool *pool; + struct git_futils_mkdir_perfdata perfdata; +}; + +/** + * Create a directory or entire path. + * + * This makes a directory (and the entire path leading up to it if requested), + * and optionally chmods the directory immediately after (or each part of the + * path if requested). + * + * @param path The path to create, relative to base. + * @param base Root for relative path. These directories will never be made. + * @param mode The mode to use for created directories. + * @param flags Combination of the mkdir flags above. + * @param opts Extended options, or null. + * @return 0 on success, else error code + */ +extern int git_futils_mkdir_relative(const char *path, const char *base, mode_t mode, uint32_t flags, struct git_futils_mkdir_options *opts); + +/** + * Create a directory or entire path. Similar to `git_futils_mkdir_relative` + * without performance data. + */ +extern int git_futils_mkdir(const char *path, mode_t mode, uint32_t flags); + +/** + * Create all the folders required to contain + * the full path of a file + */ +extern int git_futils_mkpath2file(const char *path, const mode_t mode); + +/** + * Flags to pass to `git_futils_rmdir_r`. + * + * * GIT_RMDIR_EMPTY_HIERARCHY - the default; remove hierarchy of empty + * dirs and generate error if any files are found. + * * GIT_RMDIR_REMOVE_FILES - attempt to remove files in the hierarchy. + * * GIT_RMDIR_SKIP_NONEMPTY - skip non-empty directories with no error. + * * GIT_RMDIR_EMPTY_PARENTS - remove containing directories up to base + * if removing this item leaves them empty + * * GIT_RMDIR_REMOVE_BLOCKERS - remove blocking file that causes ENOTDIR + * * GIT_RMDIR_SKIP_ROOT - don't remove root directory itself + */ +typedef enum { + GIT_RMDIR_EMPTY_HIERARCHY = 0, + GIT_RMDIR_REMOVE_FILES = (1 << 0), + GIT_RMDIR_SKIP_NONEMPTY = (1 << 1), + GIT_RMDIR_EMPTY_PARENTS = (1 << 2), + GIT_RMDIR_REMOVE_BLOCKERS = (1 << 3), + GIT_RMDIR_SKIP_ROOT = (1 << 4) +} git_futils_rmdir_flags; + +/** + * Remove path and any files and directories beneath it. + * + * @param path Path to the top level directory to process. + * @param base Root for relative path. + * @param flags Combination of git_futils_rmdir_flags values + * @return 0 on success; -1 on error. + */ +extern int git_futils_rmdir_r(const char *path, const char *base, uint32_t flags); + +/** + * Create and open a temporary file with a `_git2_` suffix in a + * protected directory; the file created will created will honor + * the current `umask`. Writes the filename into path_out. + * + * This function uses a high-quality PRNG seeded by the system's + * entropy pool _where available_ and falls back to a simple seed + * (time plus system information) when not. This is suitable for + * writing within a protected directory, but the system's safe + * temporary file creation functions should be preferred where + * available when writing into world-writable (temp) directories. + * + * @return On success, an open file descriptor, else an error code < 0. + */ +extern int git_futils_mktmp(git_str *path_out, const char *filename, mode_t mode); + +/** + * Move a file on the filesystem, create the + * destination path if it doesn't exist + */ +extern int git_futils_mv_withpath(const char *from, const char *to, const mode_t dirmode); + +/** + * Copy a file + * + * The filemode will be used for the newly created file. + */ +extern int git_futils_cp( + const char *from, + const char *to, + mode_t filemode); + +/** + * Set the files atime and mtime to the given time, or the current time + * if `ts` is NULL. + */ +extern int git_futils_touch(const char *path, time_t *when); + +/** + * Flags that can be passed to `git_futils_cp_r`. + * + * - GIT_CPDIR_CREATE_EMPTY_DIRS: create directories even if there are no + * files under them (otherwise directories will only be created lazily + * when a file inside them is copied). + * - GIT_CPDIR_COPY_SYMLINKS: copy symlinks, otherwise they are ignored. + * - GIT_CPDIR_COPY_DOTFILES: copy files with leading '.', otherwise ignored. + * - GIT_CPDIR_OVERWRITE: overwrite pre-existing files with source content, + * otherwise they are silently skipped. + * - GIT_CPDIR_CHMOD_DIRS: explicitly chmod directories to `dirmode` + * - GIT_CPDIR_SIMPLE_TO_MODE: default tries to replicate the mode of the + * source file to the target; with this flag, always use 0666 (or 0777 if + * source has exec bits set) for target. + * - GIT_CPDIR_LINK_FILES will try to use hardlinks for the files + */ +typedef enum { + GIT_CPDIR_CREATE_EMPTY_DIRS = (1u << 0), + GIT_CPDIR_COPY_SYMLINKS = (1u << 1), + GIT_CPDIR_COPY_DOTFILES = (1u << 2), + GIT_CPDIR_OVERWRITE = (1u << 3), + GIT_CPDIR_CHMOD_DIRS = (1u << 4), + GIT_CPDIR_SIMPLE_TO_MODE = (1u << 5), + GIT_CPDIR_LINK_FILES = (1u << 6) +} git_futils_cpdir_flags; + +/** + * Copy a directory tree. + * + * This copies directories and files from one root to another. You can + * pass a combination of GIT_CPDIR flags as defined above. + * + * If you pass the CHMOD flag, then the dirmode will be applied to all + * directories that are created during the copy, overriding the natural + * permissions. If you do not pass the CHMOD flag, then the dirmode + * will actually be copied from the source files and the `dirmode` arg + * will be ignored. + */ +extern int git_futils_cp_r( + const char *from, + const char *to, + uint32_t flags, + mode_t dirmode); + +/** + * Open a file readonly and set error if needed. + */ +extern int git_futils_open_ro(const char *path); + +/** + * Truncate a file, creating it if it doesn't exist. + */ +extern int git_futils_truncate(const char *path, int mode); + +/** + * Get the filesize in bytes of a file + */ +extern int git_futils_filesize(uint64_t *out, git_file fd); + +#define GIT_PERMS_IS_EXEC(MODE) (((MODE) & 0100) != 0) +#define GIT_PERMS_CANONICAL(MODE) (GIT_PERMS_IS_EXEC(MODE) ? 0755 : 0644) +#define GIT_PERMS_FOR_WRITE(MODE) (GIT_PERMS_IS_EXEC(MODE) ? 0777 : 0666) + +#define GIT_MODE_PERMS_MASK 0777 +#define GIT_MODE_TYPE_MASK 0170000 +#define GIT_MODE_TYPE(MODE) ((MODE) & GIT_MODE_TYPE_MASK) +#define GIT_MODE_ISBLOB(MODE) (GIT_MODE_TYPE(MODE) == GIT_MODE_TYPE(GIT_FILEMODE_BLOB)) + +/** + * Convert a mode_t from the OS to a legal git mode_t value. + */ +extern mode_t git_futils_canonical_mode(mode_t raw_mode); + + +/** + * Read-only map all or part of a file into memory. + * When possible this function should favor a virtual memory + * style mapping over some form of malloc()+read(), as the + * data access will be random and is not likely to touch the + * majority of the region requested. + * + * @param out buffer to populate with the mapping information. + * @param fd open descriptor to configure the mapping from. + * @param begin first byte to map, this should be page aligned. + * @param len number of bytes to map. + * @return + * - 0 on success; + * - -1 on error. + */ +extern int git_futils_mmap_ro( + git_map *out, + git_file fd, + off64_t begin, + size_t len); + +/** + * Read-only map an entire file. + * + * @param out buffer to populate with the mapping information. + * @param path path to file to be opened. + * @return + * - 0 on success; + * - GIT_ENOTFOUND if not found; + * - -1 on an unspecified OS related error. + */ +extern int git_futils_mmap_ro_file( + git_map *out, + const char *path); + +/** + * Release the memory associated with a previous memory mapping. + * @param map the mapping description previously configured. + */ +extern void git_futils_mmap_free(git_map *map); + +/** + * Create a "fake" symlink (text file containing the target path). + * + * @param target original symlink target + * @param path symlink file to be created + * @return 0 on success, -1 on error + */ +extern int git_futils_fake_symlink(const char *target, const char *path); + +/** + * A file stamp represents a snapshot of information about a file that can + * be used to test if the file changes. This portable implementation is + * based on stat data about that file, but it is possible that OS specific + * versions could be implemented in the future. + */ +typedef struct { + struct timespec mtime; + uint64_t size; + unsigned int ino; +} git_futils_filestamp; + +/** + * Compare stat information for file with reference info. + * + * This function updates the file stamp to current data for the given path + * and returns 0 if the file is up-to-date relative to the prior setting, + * 1 if the file has been changed, or GIT_ENOTFOUND if the file doesn't + * exist. This will not call git_error_set, so you must set the error if you + * plan to return an error. + * + * @param stamp File stamp to be checked + * @param path Path to stat and check if changed + * @return 0 if up-to-date, 1 if out-of-date, GIT_ENOTFOUND if cannot stat + */ +extern int git_futils_filestamp_check( + git_futils_filestamp *stamp, const char *path); + +/** + * Set or reset file stamp data + * + * This writes the target file stamp. If the source is NULL, this will set + * the target stamp to values that will definitely be out of date. If the + * source is not NULL, this copies the source values to the target. + * + * @param tgt File stamp to write to + * @param src File stamp to copy from or NULL to clear the target + */ +extern void git_futils_filestamp_set( + git_futils_filestamp *tgt, const git_futils_filestamp *src); + +/** + * Set file stamp data from stat structure + */ +extern void git_futils_filestamp_set_from_stat( + git_futils_filestamp *stamp, struct stat *st); + +/** + * `fsync` the parent directory of the given path, if `fsync` is + * supported for directories on this platform. + * + * @param path Path of the directory to sync. + * @return 0 on success, -1 on error + */ +extern int git_futils_fsync_dir(const char *path); + +/** + * `fsync` the parent directory of the given path, if `fsync` is + * supported for directories on this platform. + * + * @param path Path of the file whose parent directory should be synced. + * @return 0 on success, -1 on error + */ +extern int git_futils_fsync_parent(const char *path); + +#endif diff --git a/src/util/git2_features.h.in b/src/util/git2_features.h.in new file mode 100644 index 0000000..a84ea89 --- /dev/null +++ b/src/util/git2_features.h.in @@ -0,0 +1,68 @@ +#ifndef INCLUDE_features_h__ +#define INCLUDE_features_h__ + +#cmakedefine GIT_DEBUG_POOL 1 +#cmakedefine GIT_DEBUG_STRICT_ALLOC 1 +#cmakedefine GIT_DEBUG_STRICT_OPEN 1 + +#cmakedefine GIT_THREADS 1 +#cmakedefine GIT_WIN32_LEAKCHECK 1 + +#cmakedefine GIT_ARCH_64 1 +#cmakedefine GIT_ARCH_32 1 + +#cmakedefine GIT_USE_ICONV 1 +#cmakedefine GIT_USE_NSEC 1 +#cmakedefine GIT_USE_STAT_MTIM 1 +#cmakedefine GIT_USE_STAT_MTIMESPEC 1 +#cmakedefine GIT_USE_STAT_MTIME_NSEC 1 +#cmakedefine GIT_USE_FUTIMENS 1 + +#cmakedefine GIT_REGEX_REGCOMP_L +#cmakedefine GIT_REGEX_REGCOMP +#cmakedefine GIT_REGEX_PCRE +#cmakedefine GIT_REGEX_PCRE2 +#cmakedefine GIT_REGEX_BUILTIN 1 + +#cmakedefine GIT_QSORT_BSD +#cmakedefine GIT_QSORT_GNU +#cmakedefine GIT_QSORT_C11 +#cmakedefine GIT_QSORT_MSC + +#cmakedefine GIT_SSH 1 +#cmakedefine GIT_SSH_MEMORY_CREDENTIALS 1 + +#cmakedefine GIT_NTLM 1 +#cmakedefine GIT_GSSAPI 1 +#cmakedefine GIT_GSSFRAMEWORK 1 + +#cmakedefine GIT_WINHTTP 1 +#cmakedefine GIT_HTTPS 1 +#cmakedefine GIT_OPENSSL 1 +#cmakedefine GIT_OPENSSL_DYNAMIC 1 +#cmakedefine GIT_SECURE_TRANSPORT 1 +#cmakedefine GIT_MBEDTLS 1 +#cmakedefine GIT_SCHANNEL 1 + +#cmakedefine GIT_SHA1_COLLISIONDETECT 1 +#cmakedefine GIT_SHA1_WIN32 1 +#cmakedefine GIT_SHA1_COMMON_CRYPTO 1 +#cmakedefine GIT_SHA1_OPENSSL 1 +#cmakedefine GIT_SHA1_OPENSSL_DYNAMIC 1 +#cmakedefine GIT_SHA1_MBEDTLS 1 + +#cmakedefine GIT_SHA256_BUILTIN 1 +#cmakedefine GIT_SHA256_WIN32 1 +#cmakedefine GIT_SHA256_COMMON_CRYPTO 1 +#cmakedefine GIT_SHA256_OPENSSL 1 +#cmakedefine GIT_SHA256_OPENSSL_DYNAMIC 1 +#cmakedefine GIT_SHA256_MBEDTLS 1 + +#cmakedefine GIT_RAND_GETENTROPY 1 +#cmakedefine GIT_RAND_GETLOADAVG 1 + +#cmakedefine GIT_IO_POLL 1 +#cmakedefine GIT_IO_WSAPOLL 1 +#cmakedefine GIT_IO_SELECT 1 + +#endif diff --git a/src/util/git2_util.h b/src/util/git2_util.h new file mode 100644 index 0000000..c62dc24 --- /dev/null +++ b/src/util/git2_util.h @@ -0,0 +1,168 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_git2_util_h__ +#define INCLUDE_git2_util_h__ + +#if !defined(LIBGIT2_NO_FEATURES_H) +# include "git2_features.h" +#endif + +#include "git2/common.h" +#include "cc-compat.h" + +typedef struct git_str git_str; + +/** Declare a function as always inlined. */ +#if defined(_MSC_VER) +# define GIT_INLINE(type) static __inline type +#elif defined(__GNUC__) +# define GIT_INLINE(type) static __inline__ type +#else +# define GIT_INLINE(type) static type +#endif + +/** Support for gcc/clang __has_builtin intrinsic */ +#ifndef __has_builtin +# define __has_builtin(x) 0 +#endif + +/** + * Declare that a function's return value must be used. + * + * Used mostly to guard against potential silent bugs at runtime. This is + * recommended to be added to functions that: + * + * - Allocate / reallocate memory. This prevents memory leaks or errors where + * buffers are expected to have grown to a certain size, but could not be + * resized. + * - Acquire locks. When a lock cannot be acquired, that will almost certainly + * cause a data race / undefined behavior. + */ +#if defined(__GNUC__) +# define GIT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +#else +# define GIT_WARN_UNUSED_RESULT +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef GIT_WIN32 + +# include +# include +# include +# include +# include +# include "win32/msvc-compat.h" +# include "win32/mingw-compat.h" +# include "win32/win32-compat.h" +# include "win32/w32_common.h" +# include "win32/version.h" +# include "win32/error.h" +# ifdef GIT_THREADS +# include "win32/thread.h" +# endif + +#else + +# include +# include +# ifdef GIT_THREADS +# include +# include +# endif + +#define GIT_LIBGIT2_CALL +#define GIT_SYSTEM_CALL + +#ifdef GIT_USE_STAT_ATIMESPEC +# define st_atim st_atimespec +# define st_ctim st_ctimespec +# define st_mtim st_mtimespec +#endif + +# include + +#endif + +#include "git2/types.h" +#include "git2/errors.h" +#include "thread.h" +#include "integer.h" +#include "assert_safe.h" + +#include "posix.h" + +#define GIT_BUFSIZE_DEFAULT 65536 +#define GIT_BUFSIZE_FILEIO GIT_BUFSIZE_DEFAULT +#define GIT_BUFSIZE_FILTERIO GIT_BUFSIZE_DEFAULT +#define GIT_BUFSIZE_NETIO GIT_BUFSIZE_DEFAULT + + +/** + * Check a pointer allocation result, returning -1 if it failed. + */ +#define GIT_ERROR_CHECK_ALLOC(ptr) do { \ + if ((ptr) == NULL) { return -1; } \ + } while(0) + +/** + * Check a buffer allocation result, returning -1 if it failed. + */ +#define GIT_ERROR_CHECK_ALLOC_STR(buf) do { \ + if ((void *)(buf) == NULL || git_str_oom(buf)) { return -1; } \ + } while(0) + +/** + * Check a return value and propagate result if non-zero. + */ +#define GIT_ERROR_CHECK_ERROR(code) \ + do { int _err = (code); if (_err) return _err; } while (0) + + +/** Check for additive overflow, setting an error if would occur. */ +#define GIT_ADD_SIZET_OVERFLOW(out, one, two) \ + (git__add_sizet_overflow(out, one, two) ? (git_error_set_oom(), 1) : 0) + +/** Check for additive overflow, setting an error if would occur. */ +#define GIT_MULTIPLY_SIZET_OVERFLOW(out, nelem, elsize) \ + (git__multiply_sizet_overflow(out, nelem, elsize) ? (git_error_set_oom(), 1) : 0) + +/** Check for additive overflow, failing if it would occur. */ +#define GIT_ERROR_CHECK_ALLOC_ADD(out, one, two) \ + if (GIT_ADD_SIZET_OVERFLOW(out, one, two)) { return -1; } + +#define GIT_ERROR_CHECK_ALLOC_ADD3(out, one, two, three) \ + if (GIT_ADD_SIZET_OVERFLOW(out, one, two) || \ + GIT_ADD_SIZET_OVERFLOW(out, *(out), three)) { return -1; } + +#define GIT_ERROR_CHECK_ALLOC_ADD4(out, one, two, three, four) \ + if (GIT_ADD_SIZET_OVERFLOW(out, one, two) || \ + GIT_ADD_SIZET_OVERFLOW(out, *(out), three) || \ + GIT_ADD_SIZET_OVERFLOW(out, *(out), four)) { return -1; } + +#define GIT_ERROR_CHECK_ALLOC_ADD5(out, one, two, three, four, five) \ + if (GIT_ADD_SIZET_OVERFLOW(out, one, two) || \ + GIT_ADD_SIZET_OVERFLOW(out, *(out), three) || \ + GIT_ADD_SIZET_OVERFLOW(out, *(out), four) || \ + GIT_ADD_SIZET_OVERFLOW(out, *(out), five)) { return -1; } + +/** Check for multiplicative overflow, failing if it would occur. */ +#define GIT_ERROR_CHECK_ALLOC_MULTIPLY(out, nelem, elsize) \ + if (GIT_MULTIPLY_SIZET_OVERFLOW(out, nelem, elsize)) { return -1; } + +#include "util.h" + +#endif diff --git a/src/util/hash.c b/src/util/hash.c new file mode 100644 index 0000000..ff900ce --- /dev/null +++ b/src/util/hash.c @@ -0,0 +1,158 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "hash.h" + +int git_hash_global_init(void) +{ + if (git_hash_sha1_global_init() < 0 || + git_hash_sha256_global_init() < 0) + return -1; + + return 0; +} + +int git_hash_ctx_init(git_hash_ctx *ctx, git_hash_algorithm_t algorithm) +{ + int error; + + switch (algorithm) { + case GIT_HASH_ALGORITHM_SHA1: + error = git_hash_sha1_ctx_init(&ctx->ctx.sha1); + break; + case GIT_HASH_ALGORITHM_SHA256: + error = git_hash_sha256_ctx_init(&ctx->ctx.sha256); + break; + default: + git_error_set(GIT_ERROR_INTERNAL, "unknown hash algorithm"); + error = -1; + } + + ctx->algorithm = algorithm; + return error; +} + +void git_hash_ctx_cleanup(git_hash_ctx *ctx) +{ + switch (ctx->algorithm) { + case GIT_HASH_ALGORITHM_SHA1: + git_hash_sha1_ctx_cleanup(&ctx->ctx.sha1); + return; + case GIT_HASH_ALGORITHM_SHA256: + git_hash_sha256_ctx_cleanup(&ctx->ctx.sha256); + return; + default: + /* unreachable */ ; + } +} + +int git_hash_init(git_hash_ctx *ctx) +{ + switch (ctx->algorithm) { + case GIT_HASH_ALGORITHM_SHA1: + return git_hash_sha1_init(&ctx->ctx.sha1); + case GIT_HASH_ALGORITHM_SHA256: + return git_hash_sha256_init(&ctx->ctx.sha256); + default: + /* unreachable */ ; + } + + git_error_set(GIT_ERROR_INTERNAL, "unknown hash algorithm"); + return -1; +} + +int git_hash_update(git_hash_ctx *ctx, const void *data, size_t len) +{ + switch (ctx->algorithm) { + case GIT_HASH_ALGORITHM_SHA1: + return git_hash_sha1_update(&ctx->ctx.sha1, data, len); + case GIT_HASH_ALGORITHM_SHA256: + return git_hash_sha256_update(&ctx->ctx.sha256, data, len); + default: + /* unreachable */ ; + } + + git_error_set(GIT_ERROR_INTERNAL, "unknown hash algorithm"); + return -1; +} + +int git_hash_final(unsigned char *out, git_hash_ctx *ctx) +{ + switch (ctx->algorithm) { + case GIT_HASH_ALGORITHM_SHA1: + return git_hash_sha1_final(out, &ctx->ctx.sha1); + case GIT_HASH_ALGORITHM_SHA256: + return git_hash_sha256_final(out, &ctx->ctx.sha256); + default: + /* unreachable */ ; + } + + git_error_set(GIT_ERROR_INTERNAL, "unknown hash algorithm"); + return -1; +} + +int git_hash_buf( + unsigned char *out, + const void *data, + size_t len, + git_hash_algorithm_t algorithm) +{ + git_hash_ctx ctx; + int error = 0; + + if (git_hash_ctx_init(&ctx, algorithm) < 0) + return -1; + + if ((error = git_hash_update(&ctx, data, len)) >= 0) + error = git_hash_final(out, &ctx); + + git_hash_ctx_cleanup(&ctx); + + return error; +} + +int git_hash_vec( + unsigned char *out, + git_str_vec *vec, + size_t n, + git_hash_algorithm_t algorithm) +{ + git_hash_ctx ctx; + size_t i; + int error = 0; + + if (git_hash_ctx_init(&ctx, algorithm) < 0) + return -1; + + for (i = 0; i < n; i++) { + if ((error = git_hash_update(&ctx, vec[i].data, vec[i].len)) < 0) + goto done; + } + + error = git_hash_final(out, &ctx); + +done: + git_hash_ctx_cleanup(&ctx); + + return error; +} + +int git_hash_fmt(char *out, unsigned char *hash, size_t hash_len) +{ + static char hex[] = "0123456789abcdef"; + char *str = out; + size_t i; + + for (i = 0; i < hash_len; i++) { + *str++ = hex[hash[i] >> 4]; + *str++ = hex[hash[i] & 0x0f]; + } + + *str++ = '\0'; + + return 0; +} diff --git a/src/util/hash.h b/src/util/hash.h new file mode 100644 index 0000000..21fcaf0 --- /dev/null +++ b/src/util/hash.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_hash_h__ +#define INCLUDE_hash_h__ + +#include "git2_util.h" + +#include "hash/sha.h" + +typedef struct { + void *data; + size_t len; +} git_str_vec; + +typedef enum { + GIT_HASH_ALGORITHM_NONE = 0, + GIT_HASH_ALGORITHM_SHA1, + GIT_HASH_ALGORITHM_SHA256 +} git_hash_algorithm_t; + +#define GIT_HASH_MAX_SIZE GIT_HASH_SHA256_SIZE + +typedef struct git_hash_ctx { + union { + git_hash_sha1_ctx sha1; + git_hash_sha256_ctx sha256; + } ctx; + git_hash_algorithm_t algorithm; +} git_hash_ctx; + +int git_hash_global_init(void); + +int git_hash_ctx_init(git_hash_ctx *ctx, git_hash_algorithm_t algorithm); +void git_hash_ctx_cleanup(git_hash_ctx *ctx); + +int git_hash_init(git_hash_ctx *c); +int git_hash_update(git_hash_ctx *c, const void *data, size_t len); +int git_hash_final(unsigned char *out, git_hash_ctx *c); + +int git_hash_buf(unsigned char *out, const void *data, size_t len, git_hash_algorithm_t algorithm); +int git_hash_vec(unsigned char *out, git_str_vec *vec, size_t n, git_hash_algorithm_t algorithm); + +int git_hash_fmt(char *out, unsigned char *hash, size_t hash_len); + +GIT_INLINE(size_t) git_hash_size(git_hash_algorithm_t algorithm) { + switch (algorithm) { + case GIT_HASH_ALGORITHM_SHA1: + return GIT_HASH_SHA1_SIZE; + case GIT_HASH_ALGORITHM_SHA256: + return GIT_HASH_SHA256_SIZE; + default: + return 0; + } +} + +#endif diff --git a/src/util/hash/builtin.c b/src/util/hash/builtin.c new file mode 100644 index 0000000..cc4aa58 --- /dev/null +++ b/src/util/hash/builtin.c @@ -0,0 +1,53 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "builtin.h" + +int git_hash_sha256_global_init(void) +{ + return 0; +} + +int git_hash_sha256_ctx_init(git_hash_sha256_ctx *ctx) +{ + return git_hash_sha256_init(ctx); +} + +void git_hash_sha256_ctx_cleanup(git_hash_sha256_ctx *ctx) +{ + GIT_UNUSED(ctx); +} + +int git_hash_sha256_init(git_hash_sha256_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + if (SHA256Reset(&ctx->c)) { + git_error_set(GIT_ERROR_SHA, "SHA256 error"); + return -1; + } + return 0; +} + +int git_hash_sha256_update(git_hash_sha256_ctx *ctx, const void *data, size_t len) +{ + GIT_ASSERT_ARG(ctx); + if (SHA256Input(&ctx->c, data, len)) { + git_error_set(GIT_ERROR_SHA, "SHA256 error"); + return -1; + } + return 0; +} + +int git_hash_sha256_final(unsigned char *out, git_hash_sha256_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + if (SHA256Result(&ctx->c, out)) { + git_error_set(GIT_ERROR_SHA, "SHA256 error"); + return -1; + } + return 0; +} diff --git a/src/util/hash/builtin.h b/src/util/hash/builtin.h new file mode 100644 index 0000000..769df1a --- /dev/null +++ b/src/util/hash/builtin.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_hash_builtin_h__ +#define INCLUDE_hash_builtin_h__ + +#include "hash/sha.h" + +#include "rfc6234/sha.h" + +struct git_hash_sha256_ctx { + SHA256Context c; +}; + +#endif diff --git a/src/util/hash/collisiondetect.c b/src/util/hash/collisiondetect.c new file mode 100644 index 0000000..c51a402 --- /dev/null +++ b/src/util/hash/collisiondetect.c @@ -0,0 +1,48 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "collisiondetect.h" + +int git_hash_sha1_global_init(void) +{ + return 0; +} + +int git_hash_sha1_ctx_init(git_hash_sha1_ctx *ctx) +{ + return git_hash_sha1_init(ctx); +} + +void git_hash_sha1_ctx_cleanup(git_hash_sha1_ctx *ctx) +{ + GIT_UNUSED(ctx); +} + +int git_hash_sha1_init(git_hash_sha1_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + SHA1DCInit(&ctx->c); + return 0; +} + +int git_hash_sha1_update(git_hash_sha1_ctx *ctx, const void *data, size_t len) +{ + GIT_ASSERT_ARG(ctx); + SHA1DCUpdate(&ctx->c, data, len); + return 0; +} + +int git_hash_sha1_final(unsigned char *out, git_hash_sha1_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + if (SHA1DCFinal(out, &ctx->c)) { + git_error_set(GIT_ERROR_SHA, "SHA1 collision attack detected"); + return -1; + } + + return 0; +} diff --git a/src/util/hash/collisiondetect.h b/src/util/hash/collisiondetect.h new file mode 100644 index 0000000..8de5502 --- /dev/null +++ b/src/util/hash/collisiondetect.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_hash_collisiondetect_h__ +#define INCLUDE_hash_collisiondetect_h__ + +#include "hash/sha.h" + +#include "sha1dc/sha1.h" + +struct git_hash_sha1_ctx { + SHA1_CTX c; +}; + +#endif diff --git a/src/util/hash/common_crypto.c b/src/util/hash/common_crypto.c new file mode 100644 index 0000000..b327ba9 --- /dev/null +++ b/src/util/hash/common_crypto.c @@ -0,0 +1,112 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common_crypto.h" + +#define CC_LONG_MAX ((CC_LONG)-1) + +#ifdef GIT_SHA1_COMMON_CRYPTO + +int git_hash_sha1_global_init(void) +{ + return 0; +} + +int git_hash_sha1_ctx_init(git_hash_sha1_ctx *ctx) +{ + return git_hash_sha1_init(ctx); +} + +void git_hash_sha1_ctx_cleanup(git_hash_sha1_ctx *ctx) +{ + GIT_UNUSED(ctx); +} + +int git_hash_sha1_init(git_hash_sha1_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + CC_SHA1_Init(&ctx->c); + return 0; +} + +int git_hash_sha1_update(git_hash_sha1_ctx *ctx, const void *_data, size_t len) +{ + const unsigned char *data = _data; + + GIT_ASSERT_ARG(ctx); + + while (len > 0) { + CC_LONG chunk = (len > CC_LONG_MAX) ? CC_LONG_MAX : (CC_LONG)len; + + CC_SHA1_Update(&ctx->c, data, chunk); + + data += chunk; + len -= chunk; + } + + return 0; +} + +int git_hash_sha1_final(unsigned char *out, git_hash_sha1_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + CC_SHA1_Final(out, &ctx->c); + return 0; +} + +#endif + +#ifdef GIT_SHA256_COMMON_CRYPTO + +int git_hash_sha256_global_init(void) +{ + return 0; +} + +int git_hash_sha256_ctx_init(git_hash_sha256_ctx *ctx) +{ + return git_hash_sha256_init(ctx); +} + +void git_hash_sha256_ctx_cleanup(git_hash_sha256_ctx *ctx) +{ + GIT_UNUSED(ctx); +} + +int git_hash_sha256_init(git_hash_sha256_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + CC_SHA256_Init(&ctx->c); + return 0; +} + +int git_hash_sha256_update(git_hash_sha256_ctx *ctx, const void *_data, size_t len) +{ + const unsigned char *data = _data; + + GIT_ASSERT_ARG(ctx); + + while (len > 0) { + CC_LONG chunk = (len > CC_LONG_MAX) ? CC_LONG_MAX : (CC_LONG)len; + + CC_SHA256_Update(&ctx->c, data, chunk); + + data += chunk; + len -= chunk; + } + + return 0; +} + +int git_hash_sha256_final(unsigned char *out, git_hash_sha256_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + CC_SHA256_Final(out, &ctx->c); + return 0; +} + +#endif diff --git a/src/util/hash/common_crypto.h b/src/util/hash/common_crypto.h new file mode 100644 index 0000000..157712b --- /dev/null +++ b/src/util/hash/common_crypto.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_hash_common_crypto_h__ +#define INCLUDE_hash_common_crypto_h__ + +#include "hash/sha.h" + +#include + +#ifdef GIT_SHA1_COMMON_CRYPTO +struct git_hash_sha1_ctx { + CC_SHA1_CTX c; +}; +#endif + +#ifdef GIT_SHA256_COMMON_CRYPTO +struct git_hash_sha256_ctx { + CC_SHA256_CTX c; +}; +#endif + +#endif diff --git a/src/util/hash/mbedtls.c b/src/util/hash/mbedtls.c new file mode 100644 index 0000000..ecdfb78 --- /dev/null +++ b/src/util/hash/mbedtls.c @@ -0,0 +1,92 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "mbedtls.h" + +#ifdef GIT_SHA1_MBEDTLS + +int git_hash_sha1_global_init(void) +{ + return 0; +} + +int git_hash_sha1_ctx_init(git_hash_sha1_ctx *ctx) +{ + return git_hash_sha1_init(ctx); +} + +void git_hash_sha1_ctx_cleanup(git_hash_sha1_ctx *ctx) +{ + if (ctx) + mbedtls_sha1_free(&ctx->c); +} + +int git_hash_sha1_init(git_hash_sha1_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + mbedtls_sha1_init(&ctx->c); + mbedtls_sha1_starts(&ctx->c); + return 0; +} + +int git_hash_sha1_update(git_hash_sha1_ctx *ctx, const void *data, size_t len) +{ + GIT_ASSERT_ARG(ctx); + mbedtls_sha1_update(&ctx->c, data, len); + return 0; +} + +int git_hash_sha1_final(unsigned char *out, git_hash_sha1_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + mbedtls_sha1_finish(&ctx->c, out); + return 0; +} + +#endif + +#ifdef GIT_SHA256_MBEDTLS + +int git_hash_sha256_global_init(void) +{ + return 0; +} + +int git_hash_sha256_ctx_init(git_hash_sha256_ctx *ctx) +{ + return git_hash_sha256_init(ctx); +} + +void git_hash_sha256_ctx_cleanup(git_hash_sha256_ctx *ctx) +{ + if (ctx) + mbedtls_sha256_free(&ctx->c); +} + +int git_hash_sha256_init(git_hash_sha256_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + mbedtls_sha256_init(&ctx->c); + mbedtls_sha256_starts(&ctx->c, 0); + return 0; +} + +int git_hash_sha256_update(git_hash_sha256_ctx *ctx, const void *data, size_t len) +{ + GIT_ASSERT_ARG(ctx); + mbedtls_sha256_update(&ctx->c, data, len); + return 0; +} + +int git_hash_sha256_final(unsigned char *out, git_hash_sha256_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + mbedtls_sha256_finish(&ctx->c, out); + return 0; +} + +#endif diff --git a/src/util/hash/mbedtls.h b/src/util/hash/mbedtls.h new file mode 100644 index 0000000..05fb38b --- /dev/null +++ b/src/util/hash/mbedtls.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_hash_mbedtls_h__ +#define INCLUDE_hash_mbedtls_h__ + +#include "hash/sha.h" + +#ifdef GIT_SHA1_MBEDTLS +# include + +struct git_hash_sha1_ctx { + mbedtls_sha1_context c; +}; +#endif + +#ifdef GIT_SHA256_MBEDTLS +# include + +struct git_hash_sha256_ctx { + mbedtls_sha256_context c; +}; +#endif + +#endif /* INCLUDE_hash_sha1_mbedtls_h__ */ diff --git a/src/util/hash/openssl.c b/src/util/hash/openssl.c new file mode 100644 index 0000000..eaf91e7 --- /dev/null +++ b/src/util/hash/openssl.c @@ -0,0 +1,195 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "openssl.h" + +#ifdef GIT_OPENSSL_DYNAMIC +# include + +static int handle_count; +static void *openssl_handle; + +static int git_hash_openssl_global_shutdown(void) +{ + if (--handle_count == 0) { + dlclose(openssl_handle); + openssl_handle = NULL; + } + + return 0; +} + +static int git_hash_openssl_global_init(void) +{ + if (!handle_count) { + if ((openssl_handle = dlopen("libssl.so.1.1", RTLD_NOW)) == NULL && + (openssl_handle = dlopen("libssl.1.1.dylib", RTLD_NOW)) == NULL && + (openssl_handle = dlopen("libssl.so.1.0.0", RTLD_NOW)) == NULL && + (openssl_handle = dlopen("libssl.1.0.0.dylib", RTLD_NOW)) == NULL && + (openssl_handle = dlopen("libssl.so.10", RTLD_NOW)) == NULL && + (openssl_handle = dlopen("libssl.so.3", RTLD_NOW)) == NULL) { + git_error_set(GIT_ERROR_SSL, "could not load ssl libraries"); + return -1; + } + } + + if (git_hash_openssl_global_shutdown() < 0) + return -1; + + handle_count++; + return 0; +} + +#endif + +#ifdef GIT_SHA1_OPENSSL + +# ifdef GIT_OPENSSL_DYNAMIC +static int (*SHA1_Init)(SHA_CTX *c); +static int (*SHA1_Update)(SHA_CTX *c, const void *data, size_t len); +static int (*SHA1_Final)(unsigned char *md, SHA_CTX *c); +# endif + +int git_hash_sha1_global_init(void) +{ +#ifdef GIT_OPENSSL_DYNAMIC + if (git_hash_openssl_global_init() < 0) + return -1; + + if ((SHA1_Init = dlsym(openssl_handle, "SHA1_Init")) == NULL || + (SHA1_Update = dlsym(openssl_handle, "SHA1_Update")) == NULL || + (SHA1_Final = dlsym(openssl_handle, "SHA1_Final")) == NULL) { + const char *msg = dlerror(); + git_error_set(GIT_ERROR_SSL, "could not load hash function: %s", msg ? msg : "unknown error"); + return -1; + } +#endif + + return 0; +} + +int git_hash_sha1_ctx_init(git_hash_sha1_ctx *ctx) +{ + return git_hash_sha1_init(ctx); +} + +void git_hash_sha1_ctx_cleanup(git_hash_sha1_ctx *ctx) +{ + GIT_UNUSED(ctx); +} + +int git_hash_sha1_init(git_hash_sha1_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + + if (SHA1_Init(&ctx->c) != 1) { + git_error_set(GIT_ERROR_SHA, "failed to initialize sha1 context"); + return -1; + } + + return 0; +} + +int git_hash_sha1_update(git_hash_sha1_ctx *ctx, const void *data, size_t len) +{ + GIT_ASSERT_ARG(ctx); + + if (SHA1_Update(&ctx->c, data, len) != 1) { + git_error_set(GIT_ERROR_SHA, "failed to update sha1"); + return -1; + } + + return 0; +} + +int git_hash_sha1_final(unsigned char *out, git_hash_sha1_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + + if (SHA1_Final(out, &ctx->c) != 1) { + git_error_set(GIT_ERROR_SHA, "failed to finalize sha1"); + return -1; + } + + return 0; +} + +#endif + +#ifdef GIT_SHA256_OPENSSL + +# ifdef GIT_OPENSSL_DYNAMIC +static int (*SHA256_Init)(SHA256_CTX *c); +static int (*SHA256_Update)(SHA256_CTX *c, const void *data, size_t len); +static int (*SHA256_Final)(unsigned char *md, SHA256_CTX *c); +#endif + +int git_hash_sha256_global_init(void) +{ +#ifdef GIT_OPENSSL_DYNAMIC + if (git_hash_openssl_global_init() < 0) + return -1; + + if ((SHA256_Init = dlsym(openssl_handle, "SHA256_Init")) == NULL || + (SHA256_Update = dlsym(openssl_handle, "SHA256_Update")) == NULL || + (SHA256_Final = dlsym(openssl_handle, "SHA256_Final")) == NULL) { + const char *msg = dlerror(); + git_error_set(GIT_ERROR_SSL, "could not load hash function: %s", msg ? msg : "unknown error"); + return -1; + } +#endif + + return 0; +} + +int git_hash_sha256_ctx_init(git_hash_sha256_ctx *ctx) +{ + return git_hash_sha256_init(ctx); +} + +void git_hash_sha256_ctx_cleanup(git_hash_sha256_ctx *ctx) +{ + GIT_UNUSED(ctx); +} + +int git_hash_sha256_init(git_hash_sha256_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + + if (SHA256_Init(&ctx->c) != 1) { + git_error_set(GIT_ERROR_SHA, "failed to initialize sha256 context"); + return -1; + } + + return 0; +} + +int git_hash_sha256_update(git_hash_sha256_ctx *ctx, const void *data, size_t len) +{ + GIT_ASSERT_ARG(ctx); + + if (SHA256_Update(&ctx->c, data, len) != 1) { + git_error_set(GIT_ERROR_SHA, "failed to update sha256"); + return -1; + } + + return 0; +} + +int git_hash_sha256_final(unsigned char *out, git_hash_sha256_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + + if (SHA256_Final(out, &ctx->c) != 1) { + git_error_set(GIT_ERROR_SHA, "failed to finalize sha256"); + return -1; + } + + return 0; +} + +#endif diff --git a/src/util/hash/openssl.h b/src/util/hash/openssl.h new file mode 100644 index 0000000..7cb089a --- /dev/null +++ b/src/util/hash/openssl.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_hash_openssl_h__ +#define INCLUDE_hash_openssl_h__ + +#include "hash/sha.h" + +#ifndef GIT_OPENSSL_DYNAMIC +# include +#else + +typedef struct { + unsigned int h0, h1, h2, h3, h4; + unsigned int Nl, Nh; + unsigned int data[16]; + unsigned int num; +} SHA_CTX; + +typedef struct { + unsigned int h[8]; + unsigned int Nl, Nh; + unsigned int data[16]; + unsigned int num, md_len; +} SHA256_CTX; + +#endif + +#ifdef GIT_SHA1_OPENSSL +struct git_hash_sha1_ctx { + SHA_CTX c; +}; +#endif + +#ifdef GIT_SHA256_OPENSSL +struct git_hash_sha256_ctx { + SHA256_CTX c; +}; +#endif + +#endif diff --git a/src/util/hash/rfc6234/sha.h b/src/util/hash/rfc6234/sha.h new file mode 100644 index 0000000..0fbcc50 --- /dev/null +++ b/src/util/hash/rfc6234/sha.h @@ -0,0 +1,243 @@ +/**************************** sha.h ****************************/ +/***************** See RFC 6234 for details. *******************/ +/* + Copyright (c) 2011 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + - Redistributions of source code must retain the above + copyright notice, this list of conditions and + the following disclaimer. + + - Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + - Neither the name of Internet Society, IETF or IETF Trust, nor + the names of specific contributors, may be used to endorse or + promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#ifndef _SHA_H_ +#define _SHA_H_ + +/* + * Description: + * This file implements the Secure Hash Algorithms + * as defined in the U.S. National Institute of Standards + * and Technology Federal Information Processing Standards + * Publication (FIPS PUB) 180-3 published in October 2008 + * and formerly defined in its predecessors, FIPS PUB 180-1 + * and FIP PUB 180-2. + * + * A combined document showing all algorithms is available at + * http://csrc.nist.gov/publications/fips/ + * fips180-3/fips180-3_final.pdf + * + * The five hashes are defined in these sizes: + * SHA-1 20 byte / 160 bit + * SHA-224 28 byte / 224 bit + * SHA-256 32 byte / 256 bit + * SHA-384 48 byte / 384 bit + * SHA-512 64 byte / 512 bit + * + * Compilation Note: + * These files may be compiled with two options: + * USE_32BIT_ONLY - use 32-bit arithmetic only, for systems + * without 64-bit integers + * + * USE_MODIFIED_MACROS - use alternate form of the SHA_Ch() + * and SHA_Maj() macros that are equivalent + * and potentially faster on many systems + * + */ + +#include +/* + * If you do not have the ISO standard stdint.h header file, then you + * must typedef the following: + * name meaning + * uint64_t unsigned 64-bit integer + * uint32_t unsigned 32-bit integer + * uint8_t unsigned 8-bit integer (i.e., unsigned char) + * int_least16_t integer of >= 16 bits + * + * See stdint-example.h + */ + +#ifndef _SHA_enum_ +#define _SHA_enum_ +/* + * All SHA functions return one of these values. + */ +enum { + shaSuccess = 0, + shaNull, /* Null pointer parameter */ + shaInputTooLong, /* input data too long */ + shaStateError, /* called Input after FinalBits or Result */ + shaBadParam /* passed a bad parameter */ +}; +#endif /* _SHA_enum_ */ + +/* + * These constants hold size information for each of the SHA + * hashing operations + */ +enum { + SHA1_Message_Block_Size = 64, SHA224_Message_Block_Size = 64, + SHA256_Message_Block_Size = 64, SHA384_Message_Block_Size = 128, + SHA512_Message_Block_Size = 128, + USHA_Max_Message_Block_Size = SHA512_Message_Block_Size, + SHA1HashSize = 20, SHA224HashSize = 28, SHA256HashSize = 32, + SHA384HashSize = 48, SHA512HashSize = 64, + USHAMaxHashSize = SHA512HashSize, + + SHA1HashSizeBits = 160, SHA224HashSizeBits = 224, + SHA256HashSizeBits = 256, SHA384HashSizeBits = 384, + SHA512HashSizeBits = 512, USHAMaxHashSizeBits = SHA512HashSizeBits +}; + +/* + * These constants are used in the USHA (Unified SHA) functions. + */ +typedef enum SHAversion { + SHA1, SHA224, SHA256, SHA384, SHA512 +} SHAversion; + +/* + * This structure will hold context information for the SHA-1 + * hashing operation. + */ +typedef struct SHA1Context { + uint32_t Intermediate_Hash[SHA1HashSize/4]; /* Message Digest */ + + uint32_t Length_High; /* Message length in bits */ + uint32_t Length_Low; /* Message length in bits */ + + int_least16_t Message_Block_Index; /* Message_Block array index */ + /* 512-bit message blocks */ + uint8_t Message_Block[SHA1_Message_Block_Size]; + + int Computed; /* Is the hash computed? */ + int Corrupted; /* Cumulative corruption code */ +} SHA1Context; + +/* + * This structure will hold context information for the SHA-256 + * hashing operation. + */ +typedef struct SHA256Context { + uint32_t Intermediate_Hash[SHA256HashSize/4]; /* Message Digest */ + + uint32_t Length_High; /* Message length in bits */ + uint32_t Length_Low; /* Message length in bits */ + + int_least16_t Message_Block_Index; /* Message_Block array index */ + /* 512-bit message blocks */ + uint8_t Message_Block[SHA256_Message_Block_Size]; + + int Computed; /* Is the hash computed? */ + int Corrupted; /* Cumulative corruption code */ +} SHA256Context; + +/* + * This structure will hold context information for the SHA-512 + * hashing operation. + */ +typedef struct SHA512Context { +#ifdef USE_32BIT_ONLY + uint32_t Intermediate_Hash[SHA512HashSize/4]; /* Message Digest */ + uint32_t Length[4]; /* Message length in bits */ +#else /* !USE_32BIT_ONLY */ + uint64_t Intermediate_Hash[SHA512HashSize/8]; /* Message Digest */ + uint64_t Length_High, Length_Low; /* Message length in bits */ +#endif /* USE_32BIT_ONLY */ + + int_least16_t Message_Block_Index; /* Message_Block array index */ + /* 1024-bit message blocks */ + uint8_t Message_Block[SHA512_Message_Block_Size]; + + int Computed; /* Is the hash computed?*/ + int Corrupted; /* Cumulative corruption code */ +} SHA512Context; + +/* + * This structure will hold context information for the SHA-224 + * hashing operation. It uses the SHA-256 structure for computation. + */ +typedef struct SHA256Context SHA224Context; + +/* + * This structure will hold context information for the SHA-384 + * hashing operation. It uses the SHA-512 structure for computation. + */ +typedef struct SHA512Context SHA384Context; + +/* + * Function Prototypes + */ + +/* SHA-1 */ +extern int SHA1Reset(SHA1Context *); +extern int SHA1Input(SHA1Context *, const uint8_t *bytes, + unsigned int bytecount); +extern int SHA1FinalBits(SHA1Context *, uint8_t bits, + unsigned int bit_count); +extern int SHA1Result(SHA1Context *, + uint8_t Message_Digest[SHA1HashSize]); + +/* SHA-224 */ +extern int SHA224Reset(SHA224Context *); +extern int SHA224Input(SHA224Context *, const uint8_t *bytes, + unsigned int bytecount); +extern int SHA224FinalBits(SHA224Context *, uint8_t bits, + unsigned int bit_count); +extern int SHA224Result(SHA224Context *, + uint8_t Message_Digest[SHA224HashSize]); + +/* SHA-256 */ +extern int SHA256Reset(SHA256Context *); +extern int SHA256Input(SHA256Context *, const uint8_t *bytes, + unsigned int bytecount); +extern int SHA256FinalBits(SHA256Context *, uint8_t bits, + unsigned int bit_count); +extern int SHA256Result(SHA256Context *, + uint8_t Message_Digest[SHA256HashSize]); + +/* SHA-384 */ +extern int SHA384Reset(SHA384Context *); +extern int SHA384Input(SHA384Context *, const uint8_t *bytes, + unsigned int bytecount); +extern int SHA384FinalBits(SHA384Context *, uint8_t bits, + unsigned int bit_count); +extern int SHA384Result(SHA384Context *, + uint8_t Message_Digest[SHA384HashSize]); + +/* SHA-512 */ +extern int SHA512Reset(SHA512Context *); +extern int SHA512Input(SHA512Context *, const uint8_t *bytes, + unsigned int bytecount); +extern int SHA512FinalBits(SHA512Context *, uint8_t bits, + unsigned int bit_count); +extern int SHA512Result(SHA512Context *, + uint8_t Message_Digest[SHA512HashSize]); + +#endif /* _SHA_H_ */ diff --git a/src/util/hash/rfc6234/sha224-256.c b/src/util/hash/rfc6234/sha224-256.c new file mode 100644 index 0000000..c8e0cf8 --- /dev/null +++ b/src/util/hash/rfc6234/sha224-256.c @@ -0,0 +1,601 @@ +/************************* sha224-256.c ************************/ +/***************** See RFC 6234 for details. *******************/ +/* Copyright (c) 2011 IETF Trust and the persons identified as */ +/* authors of the code. All rights reserved. */ +/* See sha.h for terms of use and redistribution. */ + +/* + * Description: + * This file implements the Secure Hash Algorithms SHA-224 and + * SHA-256 as defined in the U.S. National Institute of Standards + * and Technology Federal Information Processing Standards + * Publication (FIPS PUB) 180-3 published in October 2008 + * and formerly defined in its predecessors, FIPS PUB 180-1 + * and FIP PUB 180-2. + * + * A combined document showing all algorithms is available at + * http://csrc.nist.gov/publications/fips/ + * fips180-3/fips180-3_final.pdf + * + * The SHA-224 and SHA-256 algorithms produce 224-bit and 256-bit + * message digests for a given data stream. It should take about + * 2**n steps to find a message with the same digest as a given + * message and 2**(n/2) to find any two messages with the same + * digest, when n is the digest size in bits. Therefore, this + * algorithm can serve as a means of providing a + * "fingerprint" for a message. + * + * Portability Issues: + * SHA-224 and SHA-256 are defined in terms of 32-bit "words". + * This code uses (included via "sha.h") to define 32- + * and 8-bit unsigned integer types. If your C compiler does not + * support 32-bit unsigned integers, this code is not + * appropriate. + * + * Caveats: + * SHA-224 and SHA-256 are designed to work with messages less + * than 2^64 bits long. This implementation uses SHA224/256Input() + * to hash the bits that are a multiple of the size of an 8-bit + * octet, and then optionally uses SHA224/256FinalBits() + * to hash the final few bits of the input. + */ + +#include "sha.h" + +/* + * These definitions are defined in FIPS 180-3, section 4.1. + * Ch() and Maj() are defined identically in sections 4.1.1, + * 4.1.2, and 4.1.3. + * + * The definitions used in FIPS 180-3 are as follows: + */ +#ifndef USE_MODIFIED_MACROS +#define SHA_Ch(x,y,z) (((x) & (y)) ^ ((~(x)) & (z))) +#define SHA_Maj(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#else /* USE_MODIFIED_MACROS */ +/* + * The following definitions are equivalent and potentially faster. + */ +#define SHA_Ch(x, y, z) (((x) & ((y) ^ (z))) ^ (z)) +#define SHA_Maj(x, y, z) (((x) & ((y) | (z))) | ((y) & (z))) +#endif /* USE_MODIFIED_MACROS */ + +#define SHA_Parity(x, y, z) ((x) ^ (y) ^ (z)) + +/* Define the SHA shift, rotate left, and rotate right macros */ +#define SHA256_SHR(bits,word) ((word) >> (bits)) +#define SHA256_ROTL(bits,word) \ + (((word) << (bits)) | ((word) >> (32-(bits)))) +#define SHA256_ROTR(bits,word) \ + (((word) >> (bits)) | ((word) << (32-(bits)))) + +/* Define the SHA SIGMA and sigma macros */ +#define SHA256_SIGMA0(word) \ + (SHA256_ROTR( 2,word) ^ SHA256_ROTR(13,word) ^ SHA256_ROTR(22,word)) +#define SHA256_SIGMA1(word) \ + (SHA256_ROTR( 6,word) ^ SHA256_ROTR(11,word) ^ SHA256_ROTR(25,word)) +#define SHA256_sigma0(word) \ + (SHA256_ROTR( 7,word) ^ SHA256_ROTR(18,word) ^ SHA256_SHR( 3,word)) +#define SHA256_sigma1(word) \ + (SHA256_ROTR(17,word) ^ SHA256_ROTR(19,word) ^ SHA256_SHR(10,word)) + +/* + * Add "length" to the length. + * Set Corrupted when overflow has occurred. + */ +static uint32_t addTemp; +#define SHA224_256AddLength(context, length) \ + (addTemp = (context)->Length_Low, (context)->Corrupted = \ + (((context)->Length_Low += (length)) < addTemp) && \ + (++(context)->Length_High == 0) ? shaInputTooLong : \ + (context)->Corrupted ) + +/* Local Function Prototypes */ +static int SHA224_256Reset(SHA256Context *context, uint32_t *H0); +static void SHA224_256ProcessMessageBlock(SHA256Context *context); +static void SHA224_256Finalize(SHA256Context *context, + uint8_t Pad_Byte); +static void SHA224_256PadMessage(SHA256Context *context, + uint8_t Pad_Byte); +static int SHA224_256ResultN(SHA256Context *context, + uint8_t Message_Digest[ ], int HashSize); + +/* Initial Hash Values: FIPS 180-3 section 5.3.2 */ +static uint32_t SHA224_H0[SHA256HashSize/4] = { + 0xC1059ED8, 0x367CD507, 0x3070DD17, 0xF70E5939, + 0xFFC00B31, 0x68581511, 0x64F98FA7, 0xBEFA4FA4 +}; + +/* Initial Hash Values: FIPS 180-3 section 5.3.3 */ +static uint32_t SHA256_H0[SHA256HashSize/4] = { + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19 +}; + +/* + * SHA224Reset + * + * Description: + * This function will initialize the SHA224Context in preparation + * for computing a new SHA224 message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * + * Returns: + * sha Error Code. + */ +int SHA224Reset(SHA224Context *context) +{ + return SHA224_256Reset(context, SHA224_H0); +} + +/* + * SHA224Input + * + * Description: + * This function accepts an array of octets as the next portion + * of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_array[ ]: [in] + * An array of octets representing the next portion of + * the message. + * length: [in] + * The length of the message in message_array. + * + * Returns: + * sha Error Code. + * + */ +int SHA224Input(SHA224Context *context, const uint8_t *message_array, + unsigned int length) +{ + return SHA256Input(context, message_array, length); +} + +/* + * SHA224FinalBits + * + * Description: + * This function will add in any final bits of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_bits: [in] + * The final bits of the message, in the upper portion of the + * byte. (Use 0b###00000 instead of 0b00000### to input the + * three bits ###.) + * length: [in] + * The number of bits in message_bits, between 1 and 7. + * + * Returns: + * sha Error Code. + */ +int SHA224FinalBits(SHA224Context *context, + uint8_t message_bits, unsigned int length) +{ + return SHA256FinalBits(context, message_bits, length); +} + +/* + * SHA224Result + * + * Description: + * This function will return the 224-bit message digest + * into the Message_Digest array provided by the caller. + * NOTE: + * The first octet of hash is stored in the element with index 0, + * the last octet of hash in the element with index 27. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA hash. + * Message_Digest[ ]: [out] + * Where the digest is returned. + * + * Returns: + * sha Error Code. + */ +int SHA224Result(SHA224Context *context, + uint8_t Message_Digest[SHA224HashSize]) +{ + return SHA224_256ResultN(context, Message_Digest, SHA224HashSize); +} + +/* + * SHA256Reset + * + * Description: + * This function will initialize the SHA256Context in preparation + * for computing a new SHA256 message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * + * Returns: + * sha Error Code. + */ +int SHA256Reset(SHA256Context *context) +{ + return SHA224_256Reset(context, SHA256_H0); +} + +/* + * SHA256Input + * + * Description: + * This function accepts an array of octets as the next portion + * of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_array[ ]: [in] + * An array of octets representing the next portion of + * the message. + * length: [in] + * The length of the message in message_array. + * + * Returns: + * sha Error Code. + */ +int SHA256Input(SHA256Context *context, const uint8_t *message_array, + unsigned int length) +{ + if (!context) return shaNull; + if (!length) return shaSuccess; + if (!message_array) return shaNull; + if (context->Computed) return context->Corrupted = shaStateError; + if (context->Corrupted) return context->Corrupted; + + while (length--) { + context->Message_Block[context->Message_Block_Index++] = + *message_array; + + if ((SHA224_256AddLength(context, 8) == shaSuccess) && + (context->Message_Block_Index == SHA256_Message_Block_Size)) + SHA224_256ProcessMessageBlock(context); + + message_array++; + } + + return context->Corrupted; + +} + +/* + * SHA256FinalBits + * + * Description: + * This function will add in any final bits of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_bits: [in] + * The final bits of the message, in the upper portion of the + * byte. (Use 0b###00000 instead of 0b00000### to input the + * three bits ###.) + * length: [in] + * The number of bits in message_bits, between 1 and 7. + * + * Returns: + * sha Error Code. + */ +int SHA256FinalBits(SHA256Context *context, + uint8_t message_bits, unsigned int length) +{ + static uint8_t masks[8] = { + /* 0 0b00000000 */ 0x00, /* 1 0b10000000 */ 0x80, + /* 2 0b11000000 */ 0xC0, /* 3 0b11100000 */ 0xE0, + /* 4 0b11110000 */ 0xF0, /* 5 0b11111000 */ 0xF8, + /* 6 0b11111100 */ 0xFC, /* 7 0b11111110 */ 0xFE + }; + static uint8_t markbit[8] = { + /* 0 0b10000000 */ 0x80, /* 1 0b01000000 */ 0x40, + /* 2 0b00100000 */ 0x20, /* 3 0b00010000 */ 0x10, + /* 4 0b00001000 */ 0x08, /* 5 0b00000100 */ 0x04, + /* 6 0b00000010 */ 0x02, /* 7 0b00000001 */ 0x01 + }; + + if (!context) return shaNull; + if (!length) return shaSuccess; + if (context->Corrupted) return context->Corrupted; + if (context->Computed) return context->Corrupted = shaStateError; + if (length >= 8) return context->Corrupted = shaBadParam; + + SHA224_256AddLength(context, length); + SHA224_256Finalize(context, (uint8_t) + ((message_bits & masks[length]) | markbit[length])); + + return context->Corrupted; +} + +/* + * SHA256Result + * + * Description: + * This function will return the 256-bit message digest + * into the Message_Digest array provided by the caller. + * NOTE: + * The first octet of hash is stored in the element with index 0, + * the last octet of hash in the element with index 31. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA hash. + * Message_Digest[ ]: [out] + * Where the digest is returned. + * + * Returns: + * sha Error Code. + */ +int SHA256Result(SHA256Context *context, + uint8_t Message_Digest[SHA256HashSize]) +{ + return SHA224_256ResultN(context, Message_Digest, SHA256HashSize); +} + +/* + * SHA224_256Reset + * + * Description: + * This helper function will initialize the SHA256Context in + * preparation for computing a new SHA-224 or SHA-256 message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * H0[ ]: [in] + * The initial hash value array to use. + * + * Returns: + * sha Error Code. + */ +static int SHA224_256Reset(SHA256Context *context, uint32_t *H0) +{ + if (!context) return shaNull; + + context->Length_High = context->Length_Low = 0; + context->Message_Block_Index = 0; + + context->Intermediate_Hash[0] = H0[0]; + context->Intermediate_Hash[1] = H0[1]; + context->Intermediate_Hash[2] = H0[2]; + context->Intermediate_Hash[3] = H0[3]; + context->Intermediate_Hash[4] = H0[4]; + context->Intermediate_Hash[5] = H0[5]; + context->Intermediate_Hash[6] = H0[6]; + context->Intermediate_Hash[7] = H0[7]; + + context->Computed = 0; + context->Corrupted = shaSuccess; + + return shaSuccess; +} + +/* + * SHA224_256ProcessMessageBlock + * + * Description: + * This helper function will process the next 512 bits of the + * message stored in the Message_Block array. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * + * Returns: + * Nothing. + * + * Comments: + * Many of the variable names in this code, especially the + * single character names, were used because those were the + * names used in the Secure Hash Standard. + */ +static void SHA224_256ProcessMessageBlock(SHA256Context *context) +{ + /* Constants defined in FIPS 180-3, section 4.2.2 */ + static const uint32_t K[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, + 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, + 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, + 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, + 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, + 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, + 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, + 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + }; + int t, t4; /* Loop counter */ + uint32_t temp1, temp2; /* Temporary word value */ + uint32_t W[64]; /* Word sequence */ + uint32_t A, B, C, D, E, F, G, H; /* Word buffers */ + + /* + * Initialize the first 16 words in the array W + */ + for (t = t4 = 0; t < 16; t++, t4 += 4) + W[t] = (((uint32_t)context->Message_Block[t4]) << 24) | + (((uint32_t)context->Message_Block[t4 + 1]) << 16) | + (((uint32_t)context->Message_Block[t4 + 2]) << 8) | + (((uint32_t)context->Message_Block[t4 + 3])); + + for (t = 16; t < 64; t++) + W[t] = SHA256_sigma1(W[t-2]) + W[t-7] + + SHA256_sigma0(W[t-15]) + W[t-16]; + + A = context->Intermediate_Hash[0]; + B = context->Intermediate_Hash[1]; + C = context->Intermediate_Hash[2]; + D = context->Intermediate_Hash[3]; + E = context->Intermediate_Hash[4]; + F = context->Intermediate_Hash[5]; + G = context->Intermediate_Hash[6]; + H = context->Intermediate_Hash[7]; + + for (t = 0; t < 64; t++) { + temp1 = H + SHA256_SIGMA1(E) + SHA_Ch(E,F,G) + K[t] + W[t]; + temp2 = SHA256_SIGMA0(A) + SHA_Maj(A,B,C); + H = G; + G = F; + F = E; + E = D + temp1; + D = C; + C = B; + B = A; + A = temp1 + temp2; + } + + context->Intermediate_Hash[0] += A; + context->Intermediate_Hash[1] += B; + context->Intermediate_Hash[2] += C; + context->Intermediate_Hash[3] += D; + context->Intermediate_Hash[4] += E; + context->Intermediate_Hash[5] += F; + context->Intermediate_Hash[6] += G; + context->Intermediate_Hash[7] += H; + + context->Message_Block_Index = 0; +} + +/* + * SHA224_256Finalize + * + * Description: + * This helper function finishes off the digest calculations. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * Pad_Byte: [in] + * The last byte to add to the message block before the 0-padding + * and length. This will contain the last bits of the message + * followed by another single bit. If the message was an + * exact multiple of 8-bits long, Pad_Byte will be 0x80. + * + * Returns: + * sha Error Code. + */ +static void SHA224_256Finalize(SHA256Context *context, + uint8_t Pad_Byte) +{ + int i; + SHA224_256PadMessage(context, Pad_Byte); + /* message may be sensitive, so clear it out */ + for (i = 0; i < SHA256_Message_Block_Size; ++i) + context->Message_Block[i] = 0; + context->Length_High = 0; /* and clear length */ + context->Length_Low = 0; + context->Computed = 1; +} + +/* + * SHA224_256PadMessage + * + * Description: + * According to the standard, the message must be padded to the next + * even multiple of 512 bits. The first padding bit must be a '1'. + * The last 64 bits represent the length of the original message. + * All bits in between should be 0. This helper function will pad + * the message according to those rules by filling the + * Message_Block array accordingly. When it returns, it can be + * assumed that the message digest has been computed. + * + * Parameters: + * context: [in/out] + * The context to pad. + * Pad_Byte: [in] + * The last byte to add to the message block before the 0-padding + * and length. This will contain the last bits of the message + * followed by another single bit. If the message was an + * exact multiple of 8-bits long, Pad_Byte will be 0x80. + * + * Returns: + * Nothing. + */ +static void SHA224_256PadMessage(SHA256Context *context, + uint8_t Pad_Byte) +{ + /* + * Check to see if the current message block is too small to hold + * the initial padding bits and length. If so, we will pad the + * block, process it, and then continue padding into a second + * block. + */ + if (context->Message_Block_Index >= (SHA256_Message_Block_Size-8)) { + context->Message_Block[context->Message_Block_Index++] = Pad_Byte; + while (context->Message_Block_Index < SHA256_Message_Block_Size) + context->Message_Block[context->Message_Block_Index++] = 0; + SHA224_256ProcessMessageBlock(context); + } else + context->Message_Block[context->Message_Block_Index++] = Pad_Byte; + + while (context->Message_Block_Index < (SHA256_Message_Block_Size-8)) + context->Message_Block[context->Message_Block_Index++] = 0; + + /* + * Store the message length as the last 8 octets + */ + context->Message_Block[56] = (uint8_t)(context->Length_High >> 24); + context->Message_Block[57] = (uint8_t)(context->Length_High >> 16); + context->Message_Block[58] = (uint8_t)(context->Length_High >> 8); + context->Message_Block[59] = (uint8_t)(context->Length_High); + context->Message_Block[60] = (uint8_t)(context->Length_Low >> 24); + context->Message_Block[61] = (uint8_t)(context->Length_Low >> 16); + context->Message_Block[62] = (uint8_t)(context->Length_Low >> 8); + context->Message_Block[63] = (uint8_t)(context->Length_Low); + + SHA224_256ProcessMessageBlock(context); +} + +/* + * SHA224_256ResultN + * + * Description: + * This helper function will return the 224-bit or 256-bit message + * digest into the Message_Digest array provided by the caller. + * NOTE: + * The first octet of hash is stored in the element with index 0, + * the last octet of hash in the element with index 27/31. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA hash. + * Message_Digest[ ]: [out] + * Where the digest is returned. + * HashSize: [in] + * The size of the hash, either 28 or 32. + * + * Returns: + * sha Error Code. + */ +static int SHA224_256ResultN(SHA256Context *context, + uint8_t Message_Digest[ ], int HashSize) +{ + int i; + + if (!context) return shaNull; + if (!Message_Digest) return shaNull; + if (context->Corrupted) return context->Corrupted; + + if (!context->Computed) + SHA224_256Finalize(context, 0x80); + + for (i = 0; i < HashSize; ++i) + Message_Digest[i] = (uint8_t) + (context->Intermediate_Hash[i>>2] >> 8 * ( 3 - ( i & 0x03 ) )); + + return shaSuccess; +} + diff --git a/src/util/hash/sha.h b/src/util/hash/sha.h new file mode 100644 index 0000000..4f59623 --- /dev/null +++ b/src/util/hash/sha.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_hash_sha_h__ +#define INCLUDE_hash_sha_h__ + +#include "git2_util.h" + +typedef struct git_hash_sha1_ctx git_hash_sha1_ctx; +typedef struct git_hash_sha256_ctx git_hash_sha256_ctx; + +#if defined(GIT_SHA1_COMMON_CRYPTO) || defined(GIT_SHA256_COMMON_CRYPTO) +# include "common_crypto.h" +#endif + +#if defined(GIT_SHA1_OPENSSL) || defined(GIT_SHA256_OPENSSL) +# include "openssl.h" +#endif + +#if defined(GIT_SHA1_WIN32) || defined(GIT_SHA256_WIN32) +# include "win32.h" +#endif + +#if defined(GIT_SHA1_MBEDTLS) || defined(GIT_SHA256_MBEDTLS) +# include "mbedtls.h" +#endif + +#if defined(GIT_SHA1_COLLISIONDETECT) +# include "collisiondetect.h" +#endif + +#if defined(GIT_SHA256_BUILTIN) +# include "builtin.h" +#endif + +/* + * SHA1 + */ + +#define GIT_HASH_SHA1_SIZE 20 + +int git_hash_sha1_global_init(void); + +int git_hash_sha1_ctx_init(git_hash_sha1_ctx *ctx); +void git_hash_sha1_ctx_cleanup(git_hash_sha1_ctx *ctx); + +int git_hash_sha1_init(git_hash_sha1_ctx *c); +int git_hash_sha1_update(git_hash_sha1_ctx *c, const void *data, size_t len); +int git_hash_sha1_final(unsigned char *out, git_hash_sha1_ctx *c); + +/* + * SHA256 + */ + +#define GIT_HASH_SHA256_SIZE 32 + +int git_hash_sha256_global_init(void); + +int git_hash_sha256_ctx_init(git_hash_sha256_ctx *ctx); +void git_hash_sha256_ctx_cleanup(git_hash_sha256_ctx *ctx); + +int git_hash_sha256_init(git_hash_sha256_ctx *c); +int git_hash_sha256_update(git_hash_sha256_ctx *c, const void *data, size_t len); +int git_hash_sha256_final(unsigned char *out, git_hash_sha256_ctx *c); + +#endif diff --git a/src/util/hash/sha1dc/sha1.c b/src/util/hash/sha1dc/sha1.c new file mode 100644 index 0000000..9298227 --- /dev/null +++ b/src/util/hash/sha1dc/sha1.c @@ -0,0 +1,1909 @@ +/*** +* Copyright 2017 Marc Stevens , Dan Shumow (danshu@microsoft.com) +* Distributed under the MIT Software License. +* See accompanying file LICENSE.txt or copy at +* https://opensource.org/licenses/MIT +***/ + +#ifndef SHA1DC_NO_STANDARD_INCLUDES +#include +#include +#include +#include +#include /* make sure macros like _BIG_ENDIAN visible */ +#endif + +#ifdef SHA1DC_CUSTOM_INCLUDE_SHA1_C +#include SHA1DC_CUSTOM_INCLUDE_SHA1_C +#endif + +#ifndef SHA1DC_INIT_SAFE_HASH_DEFAULT +#define SHA1DC_INIT_SAFE_HASH_DEFAULT 1 +#endif + +#include "sha1.h" +#include "ubc_check.h" + +#if (defined(__amd64__) || defined(__amd64) || defined(__x86_64__) || defined(__x86_64) || \ + defined(i386) || defined(__i386) || defined(__i386__) || defined(__i486__) || \ + defined(__i586__) || defined(__i686__) || defined(_M_IX86) || defined(__X86__) || \ + defined(_X86_) || defined(__THW_INTEL__) || defined(__I86__) || defined(__INTEL__) || \ + defined(__386) || defined(_M_X64) || defined(_M_AMD64)) +#define SHA1DC_ON_INTEL_LIKE_PROCESSOR +#endif + +/* + Because Little-Endian architectures are most common, + we only set SHA1DC_BIGENDIAN if one of these conditions is met. + Note that all MSFT platforms are little endian, + so none of these will be defined under the MSC compiler. + If you are compiling on a big endian platform and your compiler does not define one of these, + you will have to add whatever macros your tool chain defines to indicate Big-Endianness. + */ + +#if defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) +/* + * Should detect Big Endian under GCC since at least 4.6.0 (gcc svn + * rev #165881). See + * https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html + * + * This also works under clang since 3.2, it copied the GCC-ism. See + * clang.git's 3b198a97d2 ("Preprocessor: add __BYTE_ORDER__ + * predefined macro", 2012-07-27) + */ +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define SHA1DC_BIGENDIAN +#endif + +/* Not under GCC-alike */ +#elif defined(__BYTE_ORDER) && defined(__BIG_ENDIAN) +/* + * Should detect Big Endian under glibc.git since 14245eb70e ("entered + * into RCS", 1992-11-25). Defined in which will have been + * brought in by standard headers. See glibc.git and + * https://sourceforge.net/p/predef/wiki/Endianness/ + */ +#if __BYTE_ORDER == __BIG_ENDIAN +#define SHA1DC_BIGENDIAN +#endif + +/* Not under GCC-alike or glibc */ +#elif defined(_BYTE_ORDER) && defined(_BIG_ENDIAN) && defined(_LITTLE_ENDIAN) +/* + * *BSD and newlib (embedded linux, cygwin, etc). + * the defined(_BIG_ENDIAN) && defined(_LITTLE_ENDIAN) part prevents + * this condition from matching with Solaris/sparc. + * (Solaris defines only one endian macro) + */ +#if _BYTE_ORDER == _BIG_ENDIAN +#define SHA1DC_BIGENDIAN +#endif + +/* Not under GCC-alike or glibc or *BSD or newlib */ +#elif (defined(__ARMEB__) || defined(__THUMBEB__) || defined(__AARCH64EB__) || \ + defined(__MIPSEB__) || defined(__MIPSEB) || defined(_MIPSEB) || \ + defined(__sparc)) +/* + * Should define Big Endian for a whitelist of known processors. See + * https://sourceforge.net/p/predef/wiki/Endianness/ and + * http://www.oracle.com/technetwork/server-storage/solaris/portingtosolaris-138514.html + */ +#define SHA1DC_BIGENDIAN + +/* Not under GCC-alike or glibc or *BSD or newlib or */ +#elif (defined(_AIX) || defined(__hpux)) + +/* + * Defines Big Endian on a whitelist of OSs that are known to be Big + * Endian-only. See + * https://public-inbox.org/git/93056823-2740-d072-1ebd-46b440b33d7e@felt.demon.nl/ + */ +#define SHA1DC_BIGENDIAN + +/* Not under GCC-alike or glibc or *BSD or newlib or or */ +#elif defined(SHA1DC_ON_INTEL_LIKE_PROCESSOR) +/* + * As a last resort before we do anything else we're not 100% sure + * about below, we blacklist specific processors here. We could add + * more, see e.g. https://wiki.debian.org/ArchitectureSpecificsMemo + */ +#else /* Not under GCC-alike or glibc or *BSD or newlib or or or */ + +/* We do nothing more here for now */ +/*#error "Uncomment this to see if you fall through all the detection"*/ + +#endif /* Big Endian detection */ + +#if (defined(SHA1DC_FORCE_LITTLEENDIAN) && defined(SHA1DC_BIGENDIAN)) +#undef SHA1DC_BIGENDIAN +#endif +#if (defined(SHA1DC_FORCE_BIGENDIAN) && !defined(SHA1DC_BIGENDIAN)) +#define SHA1DC_BIGENDIAN +#endif +/*ENDIANNESS SELECTION*/ + +#ifndef SHA1DC_FORCE_ALIGNED_ACCESS +#if defined(SHA1DC_FORCE_UNALIGNED_ACCESS) || defined(SHA1DC_ON_INTEL_LIKE_PROCESSOR) +#define SHA1DC_ALLOW_UNALIGNED_ACCESS +#endif /*UNALIGNED ACCESS DETECTION*/ +#endif /*FORCE ALIGNED ACCESS*/ + +#define rotate_right(x,n) (((x)>>(n))|((x)<<(32-(n)))) +#define rotate_left(x,n) (((x)<<(n))|((x)>>(32-(n)))) + +#define sha1_bswap32(x) \ + {x = ((x << 8) & 0xFF00FF00) | ((x >> 8) & 0xFF00FF); x = (x << 16) | (x >> 16);} + +#define sha1_mix(W, t) (rotate_left(W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16], 1)) + +#ifdef SHA1DC_BIGENDIAN + #define sha1_load(m, t, temp) { temp = m[t]; } +#else + #define sha1_load(m, t, temp) { temp = m[t]; sha1_bswap32(temp); } +#endif + +#define sha1_store(W, t, x) *(volatile uint32_t *)&W[t] = x + +#define sha1_f1(b,c,d) ((d)^((b)&((c)^(d)))) +#define sha1_f2(b,c,d) ((b)^(c)^(d)) +#define sha1_f3(b,c,d) (((b)&(c))+((d)&((b)^(c)))) +#define sha1_f4(b,c,d) ((b)^(c)^(d)) + +#define HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, m, t) \ + { e += rotate_left(a, 5) + sha1_f1(b,c,d) + 0x5A827999 + m[t]; b = rotate_left(b, 30); } +#define HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, m, t) \ + { e += rotate_left(a, 5) + sha1_f2(b,c,d) + 0x6ED9EBA1 + m[t]; b = rotate_left(b, 30); } +#define HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, m, t) \ + { e += rotate_left(a, 5) + sha1_f3(b,c,d) + 0x8F1BBCDC + m[t]; b = rotate_left(b, 30); } +#define HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, m, t) \ + { e += rotate_left(a, 5) + sha1_f4(b,c,d) + 0xCA62C1D6 + m[t]; b = rotate_left(b, 30); } + +#define HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(a, b, c, d, e, m, t) \ + { b = rotate_right(b, 30); e -= rotate_left(a, 5) + sha1_f1(b,c,d) + 0x5A827999 + m[t]; } +#define HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(a, b, c, d, e, m, t) \ + { b = rotate_right(b, 30); e -= rotate_left(a, 5) + sha1_f2(b,c,d) + 0x6ED9EBA1 + m[t]; } +#define HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(a, b, c, d, e, m, t) \ + { b = rotate_right(b, 30); e -= rotate_left(a, 5) + sha1_f3(b,c,d) + 0x8F1BBCDC + m[t]; } +#define HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(a, b, c, d, e, m, t) \ + { b = rotate_right(b, 30); e -= rotate_left(a, 5) + sha1_f4(b,c,d) + 0xCA62C1D6 + m[t]; } + +#define SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(a, b, c, d, e, m, W, t, temp) \ + {sha1_load(m, t, temp); sha1_store(W, t, temp); e += temp + rotate_left(a, 5) + sha1_f1(b,c,d) + 0x5A827999; b = rotate_left(b, 30);} + +#define SHA1COMPRESS_FULL_ROUND1_STEP_EXPAND(a, b, c, d, e, W, t, temp) \ + {temp = sha1_mix(W, t); sha1_store(W, t, temp); e += temp + rotate_left(a, 5) + sha1_f1(b,c,d) + 0x5A827999; b = rotate_left(b, 30); } + +#define SHA1COMPRESS_FULL_ROUND2_STEP(a, b, c, d, e, W, t, temp) \ + {temp = sha1_mix(W, t); sha1_store(W, t, temp); e += temp + rotate_left(a, 5) + sha1_f2(b,c,d) + 0x6ED9EBA1; b = rotate_left(b, 30); } + +#define SHA1COMPRESS_FULL_ROUND3_STEP(a, b, c, d, e, W, t, temp) \ + {temp = sha1_mix(W, t); sha1_store(W, t, temp); e += temp + rotate_left(a, 5) + sha1_f3(b,c,d) + 0x8F1BBCDC; b = rotate_left(b, 30); } + +#define SHA1COMPRESS_FULL_ROUND4_STEP(a, b, c, d, e, W, t, temp) \ + {temp = sha1_mix(W, t); sha1_store(W, t, temp); e += temp + rotate_left(a, 5) + sha1_f4(b,c,d) + 0xCA62C1D6; b = rotate_left(b, 30); } + + +#define SHA1_STORE_STATE(i) states[i][0] = a; states[i][1] = b; states[i][2] = c; states[i][3] = d; states[i][4] = e; + +#ifdef BUILDNOCOLLDETECTSHA1COMPRESSION +void sha1_compression(uint32_t ihv[5], const uint32_t m[16]) +{ + uint32_t W[80]; + uint32_t a,b,c,d,e; + unsigned i; + + memcpy(W, m, 16 * 4); + for (i = 16; i < 80; ++i) + W[i] = sha1_mix(W, i); + + a = ihv[0]; b = ihv[1]; c = ihv[2]; d = ihv[3]; e = ihv[4]; + + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, W, 0); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, W, 1); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, W, 2); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, W, 3); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, W, 4); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, W, 5); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, W, 6); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, W, 7); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, W, 8); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, W, 9); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, W, 10); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, W, 11); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, W, 12); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, W, 13); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, W, 14); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, W, 15); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, W, 16); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, W, 17); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, W, 18); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, W, 19); + + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, W, 20); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, W, 21); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, W, 22); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, W, 23); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, W, 24); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, W, 25); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, W, 26); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, W, 27); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, W, 28); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, W, 29); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, W, 30); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, W, 31); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, W, 32); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, W, 33); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, W, 34); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, W, 35); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, W, 36); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, W, 37); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, W, 38); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, W, 39); + + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, W, 40); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, W, 41); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, W, 42); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, W, 43); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, W, 44); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, W, 45); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, W, 46); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, W, 47); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, W, 48); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, W, 49); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, W, 50); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, W, 51); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, W, 52); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, W, 53); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, W, 54); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, W, 55); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, W, 56); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, W, 57); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, W, 58); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, W, 59); + + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, W, 60); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, W, 61); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, W, 62); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, W, 63); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, W, 64); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, W, 65); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, W, 66); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, W, 67); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, W, 68); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, W, 69); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, W, 70); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, W, 71); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, W, 72); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, W, 73); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, W, 74); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, W, 75); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, W, 76); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, W, 77); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, W, 78); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, W, 79); + + ihv[0] += a; ihv[1] += b; ihv[2] += c; ihv[3] += d; ihv[4] += e; +} +#endif /*BUILDNOCOLLDETECTSHA1COMPRESSION*/ + + +static void sha1_compression_W(uint32_t ihv[5], const uint32_t W[80]) +{ + uint32_t a = ihv[0], b = ihv[1], c = ihv[2], d = ihv[3], e = ihv[4]; + + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, W, 0); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, W, 1); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, W, 2); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, W, 3); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, W, 4); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, W, 5); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, W, 6); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, W, 7); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, W, 8); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, W, 9); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, W, 10); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, W, 11); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, W, 12); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, W, 13); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, W, 14); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, W, 15); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, W, 16); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, W, 17); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, W, 18); + HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, W, 19); + + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, W, 20); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, W, 21); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, W, 22); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, W, 23); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, W, 24); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, W, 25); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, W, 26); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, W, 27); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, W, 28); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, W, 29); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, W, 30); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, W, 31); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, W, 32); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, W, 33); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, W, 34); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, W, 35); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, W, 36); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, W, 37); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, W, 38); + HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, W, 39); + + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, W, 40); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, W, 41); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, W, 42); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, W, 43); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, W, 44); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, W, 45); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, W, 46); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, W, 47); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, W, 48); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, W, 49); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, W, 50); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, W, 51); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, W, 52); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, W, 53); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, W, 54); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, W, 55); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, W, 56); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, W, 57); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, W, 58); + HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, W, 59); + + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, W, 60); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, W, 61); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, W, 62); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, W, 63); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, W, 64); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, W, 65); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, W, 66); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, W, 67); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, W, 68); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, W, 69); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, W, 70); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, W, 71); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, W, 72); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, W, 73); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, W, 74); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, W, 75); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, W, 76); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, W, 77); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, W, 78); + HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, W, 79); + + ihv[0] += a; ihv[1] += b; ihv[2] += c; ihv[3] += d; ihv[4] += e; +} + + + +void sha1_compression_states(uint32_t ihv[5], const uint32_t m[16], uint32_t W[80], uint32_t states[80][5]) +{ + uint32_t a = ihv[0], b = ihv[1], c = ihv[2], d = ihv[3], e = ihv[4]; + uint32_t temp; + +#ifdef DOSTORESTATE00 + SHA1_STORE_STATE(0) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(a, b, c, d, e, m, W, 0, temp); + +#ifdef DOSTORESTATE01 + SHA1_STORE_STATE(1) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(e, a, b, c, d, m, W, 1, temp); + +#ifdef DOSTORESTATE02 + SHA1_STORE_STATE(2) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(d, e, a, b, c, m, W, 2, temp); + +#ifdef DOSTORESTATE03 + SHA1_STORE_STATE(3) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(c, d, e, a, b, m, W, 3, temp); + +#ifdef DOSTORESTATE04 + SHA1_STORE_STATE(4) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(b, c, d, e, a, m, W, 4, temp); + +#ifdef DOSTORESTATE05 + SHA1_STORE_STATE(5) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(a, b, c, d, e, m, W, 5, temp); + +#ifdef DOSTORESTATE06 + SHA1_STORE_STATE(6) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(e, a, b, c, d, m, W, 6, temp); + +#ifdef DOSTORESTATE07 + SHA1_STORE_STATE(7) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(d, e, a, b, c, m, W, 7, temp); + +#ifdef DOSTORESTATE08 + SHA1_STORE_STATE(8) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(c, d, e, a, b, m, W, 8, temp); + +#ifdef DOSTORESTATE09 + SHA1_STORE_STATE(9) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(b, c, d, e, a, m, W, 9, temp); + +#ifdef DOSTORESTATE10 + SHA1_STORE_STATE(10) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(a, b, c, d, e, m, W, 10, temp); + +#ifdef DOSTORESTATE11 + SHA1_STORE_STATE(11) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(e, a, b, c, d, m, W, 11, temp); + +#ifdef DOSTORESTATE12 + SHA1_STORE_STATE(12) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(d, e, a, b, c, m, W, 12, temp); + +#ifdef DOSTORESTATE13 + SHA1_STORE_STATE(13) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(c, d, e, a, b, m, W, 13, temp); + +#ifdef DOSTORESTATE14 + SHA1_STORE_STATE(14) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(b, c, d, e, a, m, W, 14, temp); + +#ifdef DOSTORESTATE15 + SHA1_STORE_STATE(15) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_LOAD(a, b, c, d, e, m, W, 15, temp); + +#ifdef DOSTORESTATE16 + SHA1_STORE_STATE(16) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_EXPAND(e, a, b, c, d, W, 16, temp); + +#ifdef DOSTORESTATE17 + SHA1_STORE_STATE(17) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_EXPAND(d, e, a, b, c, W, 17, temp); + +#ifdef DOSTORESTATE18 + SHA1_STORE_STATE(18) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_EXPAND(c, d, e, a, b, W, 18, temp); + +#ifdef DOSTORESTATE19 + SHA1_STORE_STATE(19) +#endif + SHA1COMPRESS_FULL_ROUND1_STEP_EXPAND(b, c, d, e, a, W, 19, temp); + + + +#ifdef DOSTORESTATE20 + SHA1_STORE_STATE(20) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(a, b, c, d, e, W, 20, temp); + +#ifdef DOSTORESTATE21 + SHA1_STORE_STATE(21) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(e, a, b, c, d, W, 21, temp); + +#ifdef DOSTORESTATE22 + SHA1_STORE_STATE(22) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(d, e, a, b, c, W, 22, temp); + +#ifdef DOSTORESTATE23 + SHA1_STORE_STATE(23) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(c, d, e, a, b, W, 23, temp); + +#ifdef DOSTORESTATE24 + SHA1_STORE_STATE(24) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(b, c, d, e, a, W, 24, temp); + +#ifdef DOSTORESTATE25 + SHA1_STORE_STATE(25) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(a, b, c, d, e, W, 25, temp); + +#ifdef DOSTORESTATE26 + SHA1_STORE_STATE(26) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(e, a, b, c, d, W, 26, temp); + +#ifdef DOSTORESTATE27 + SHA1_STORE_STATE(27) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(d, e, a, b, c, W, 27, temp); + +#ifdef DOSTORESTATE28 + SHA1_STORE_STATE(28) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(c, d, e, a, b, W, 28, temp); + +#ifdef DOSTORESTATE29 + SHA1_STORE_STATE(29) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(b, c, d, e, a, W, 29, temp); + +#ifdef DOSTORESTATE30 + SHA1_STORE_STATE(30) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(a, b, c, d, e, W, 30, temp); + +#ifdef DOSTORESTATE31 + SHA1_STORE_STATE(31) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(e, a, b, c, d, W, 31, temp); + +#ifdef DOSTORESTATE32 + SHA1_STORE_STATE(32) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(d, e, a, b, c, W, 32, temp); + +#ifdef DOSTORESTATE33 + SHA1_STORE_STATE(33) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(c, d, e, a, b, W, 33, temp); + +#ifdef DOSTORESTATE34 + SHA1_STORE_STATE(34) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(b, c, d, e, a, W, 34, temp); + +#ifdef DOSTORESTATE35 + SHA1_STORE_STATE(35) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(a, b, c, d, e, W, 35, temp); + +#ifdef DOSTORESTATE36 + SHA1_STORE_STATE(36) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(e, a, b, c, d, W, 36, temp); + +#ifdef DOSTORESTATE37 + SHA1_STORE_STATE(37) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(d, e, a, b, c, W, 37, temp); + +#ifdef DOSTORESTATE38 + SHA1_STORE_STATE(38) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(c, d, e, a, b, W, 38, temp); + +#ifdef DOSTORESTATE39 + SHA1_STORE_STATE(39) +#endif + SHA1COMPRESS_FULL_ROUND2_STEP(b, c, d, e, a, W, 39, temp); + + + +#ifdef DOSTORESTATE40 + SHA1_STORE_STATE(40) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(a, b, c, d, e, W, 40, temp); + +#ifdef DOSTORESTATE41 + SHA1_STORE_STATE(41) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(e, a, b, c, d, W, 41, temp); + +#ifdef DOSTORESTATE42 + SHA1_STORE_STATE(42) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(d, e, a, b, c, W, 42, temp); + +#ifdef DOSTORESTATE43 + SHA1_STORE_STATE(43) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(c, d, e, a, b, W, 43, temp); + +#ifdef DOSTORESTATE44 + SHA1_STORE_STATE(44) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(b, c, d, e, a, W, 44, temp); + +#ifdef DOSTORESTATE45 + SHA1_STORE_STATE(45) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(a, b, c, d, e, W, 45, temp); + +#ifdef DOSTORESTATE46 + SHA1_STORE_STATE(46) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(e, a, b, c, d, W, 46, temp); + +#ifdef DOSTORESTATE47 + SHA1_STORE_STATE(47) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(d, e, a, b, c, W, 47, temp); + +#ifdef DOSTORESTATE48 + SHA1_STORE_STATE(48) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(c, d, e, a, b, W, 48, temp); + +#ifdef DOSTORESTATE49 + SHA1_STORE_STATE(49) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(b, c, d, e, a, W, 49, temp); + +#ifdef DOSTORESTATE50 + SHA1_STORE_STATE(50) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(a, b, c, d, e, W, 50, temp); + +#ifdef DOSTORESTATE51 + SHA1_STORE_STATE(51) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(e, a, b, c, d, W, 51, temp); + +#ifdef DOSTORESTATE52 + SHA1_STORE_STATE(52) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(d, e, a, b, c, W, 52, temp); + +#ifdef DOSTORESTATE53 + SHA1_STORE_STATE(53) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(c, d, e, a, b, W, 53, temp); + +#ifdef DOSTORESTATE54 + SHA1_STORE_STATE(54) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(b, c, d, e, a, W, 54, temp); + +#ifdef DOSTORESTATE55 + SHA1_STORE_STATE(55) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(a, b, c, d, e, W, 55, temp); + +#ifdef DOSTORESTATE56 + SHA1_STORE_STATE(56) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(e, a, b, c, d, W, 56, temp); + +#ifdef DOSTORESTATE57 + SHA1_STORE_STATE(57) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(d, e, a, b, c, W, 57, temp); + +#ifdef DOSTORESTATE58 + SHA1_STORE_STATE(58) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(c, d, e, a, b, W, 58, temp); + +#ifdef DOSTORESTATE59 + SHA1_STORE_STATE(59) +#endif + SHA1COMPRESS_FULL_ROUND3_STEP(b, c, d, e, a, W, 59, temp); + + + + +#ifdef DOSTORESTATE60 + SHA1_STORE_STATE(60) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(a, b, c, d, e, W, 60, temp); + +#ifdef DOSTORESTATE61 + SHA1_STORE_STATE(61) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(e, a, b, c, d, W, 61, temp); + +#ifdef DOSTORESTATE62 + SHA1_STORE_STATE(62) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(d, e, a, b, c, W, 62, temp); + +#ifdef DOSTORESTATE63 + SHA1_STORE_STATE(63) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(c, d, e, a, b, W, 63, temp); + +#ifdef DOSTORESTATE64 + SHA1_STORE_STATE(64) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(b, c, d, e, a, W, 64, temp); + +#ifdef DOSTORESTATE65 + SHA1_STORE_STATE(65) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(a, b, c, d, e, W, 65, temp); + +#ifdef DOSTORESTATE66 + SHA1_STORE_STATE(66) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(e, a, b, c, d, W, 66, temp); + +#ifdef DOSTORESTATE67 + SHA1_STORE_STATE(67) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(d, e, a, b, c, W, 67, temp); + +#ifdef DOSTORESTATE68 + SHA1_STORE_STATE(68) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(c, d, e, a, b, W, 68, temp); + +#ifdef DOSTORESTATE69 + SHA1_STORE_STATE(69) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(b, c, d, e, a, W, 69, temp); + +#ifdef DOSTORESTATE70 + SHA1_STORE_STATE(70) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(a, b, c, d, e, W, 70, temp); + +#ifdef DOSTORESTATE71 + SHA1_STORE_STATE(71) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(e, a, b, c, d, W, 71, temp); + +#ifdef DOSTORESTATE72 + SHA1_STORE_STATE(72) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(d, e, a, b, c, W, 72, temp); + +#ifdef DOSTORESTATE73 + SHA1_STORE_STATE(73) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(c, d, e, a, b, W, 73, temp); + +#ifdef DOSTORESTATE74 + SHA1_STORE_STATE(74) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(b, c, d, e, a, W, 74, temp); + +#ifdef DOSTORESTATE75 + SHA1_STORE_STATE(75) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(a, b, c, d, e, W, 75, temp); + +#ifdef DOSTORESTATE76 + SHA1_STORE_STATE(76) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(e, a, b, c, d, W, 76, temp); + +#ifdef DOSTORESTATE77 + SHA1_STORE_STATE(77) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(d, e, a, b, c, W, 77, temp); + +#ifdef DOSTORESTATE78 + SHA1_STORE_STATE(78) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(c, d, e, a, b, W, 78, temp); + +#ifdef DOSTORESTATE79 + SHA1_STORE_STATE(79) +#endif + SHA1COMPRESS_FULL_ROUND4_STEP(b, c, d, e, a, W, 79, temp); + + + + ihv[0] += a; ihv[1] += b; ihv[2] += c; ihv[3] += d; ihv[4] += e; +} + + + + +#define SHA1_RECOMPRESS(t) \ +static void sha1recompress_fast_ ## t (uint32_t ihvin[5], uint32_t ihvout[5], const uint32_t me2[80], const uint32_t state[5]) \ +{ \ + uint32_t a = state[0], b = state[1], c = state[2], d = state[3], e = state[4]; \ + if (t > 79) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(b, c, d, e, a, me2, 79); \ + if (t > 78) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(c, d, e, a, b, me2, 78); \ + if (t > 77) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(d, e, a, b, c, me2, 77); \ + if (t > 76) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(e, a, b, c, d, me2, 76); \ + if (t > 75) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(a, b, c, d, e, me2, 75); \ + if (t > 74) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(b, c, d, e, a, me2, 74); \ + if (t > 73) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(c, d, e, a, b, me2, 73); \ + if (t > 72) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(d, e, a, b, c, me2, 72); \ + if (t > 71) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(e, a, b, c, d, me2, 71); \ + if (t > 70) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(a, b, c, d, e, me2, 70); \ + if (t > 69) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(b, c, d, e, a, me2, 69); \ + if (t > 68) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(c, d, e, a, b, me2, 68); \ + if (t > 67) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(d, e, a, b, c, me2, 67); \ + if (t > 66) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(e, a, b, c, d, me2, 66); \ + if (t > 65) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(a, b, c, d, e, me2, 65); \ + if (t > 64) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(b, c, d, e, a, me2, 64); \ + if (t > 63) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(c, d, e, a, b, me2, 63); \ + if (t > 62) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(d, e, a, b, c, me2, 62); \ + if (t > 61) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(e, a, b, c, d, me2, 61); \ + if (t > 60) HASHCLASH_SHA1COMPRESS_ROUND4_STEP_BW(a, b, c, d, e, me2, 60); \ + if (t > 59) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(b, c, d, e, a, me2, 59); \ + if (t > 58) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(c, d, e, a, b, me2, 58); \ + if (t > 57) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(d, e, a, b, c, me2, 57); \ + if (t > 56) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(e, a, b, c, d, me2, 56); \ + if (t > 55) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(a, b, c, d, e, me2, 55); \ + if (t > 54) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(b, c, d, e, a, me2, 54); \ + if (t > 53) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(c, d, e, a, b, me2, 53); \ + if (t > 52) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(d, e, a, b, c, me2, 52); \ + if (t > 51) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(e, a, b, c, d, me2, 51); \ + if (t > 50) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(a, b, c, d, e, me2, 50); \ + if (t > 49) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(b, c, d, e, a, me2, 49); \ + if (t > 48) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(c, d, e, a, b, me2, 48); \ + if (t > 47) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(d, e, a, b, c, me2, 47); \ + if (t > 46) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(e, a, b, c, d, me2, 46); \ + if (t > 45) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(a, b, c, d, e, me2, 45); \ + if (t > 44) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(b, c, d, e, a, me2, 44); \ + if (t > 43) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(c, d, e, a, b, me2, 43); \ + if (t > 42) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(d, e, a, b, c, me2, 42); \ + if (t > 41) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(e, a, b, c, d, me2, 41); \ + if (t > 40) HASHCLASH_SHA1COMPRESS_ROUND3_STEP_BW(a, b, c, d, e, me2, 40); \ + if (t > 39) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(b, c, d, e, a, me2, 39); \ + if (t > 38) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(c, d, e, a, b, me2, 38); \ + if (t > 37) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(d, e, a, b, c, me2, 37); \ + if (t > 36) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(e, a, b, c, d, me2, 36); \ + if (t > 35) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(a, b, c, d, e, me2, 35); \ + if (t > 34) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(b, c, d, e, a, me2, 34); \ + if (t > 33) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(c, d, e, a, b, me2, 33); \ + if (t > 32) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(d, e, a, b, c, me2, 32); \ + if (t > 31) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(e, a, b, c, d, me2, 31); \ + if (t > 30) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(a, b, c, d, e, me2, 30); \ + if (t > 29) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(b, c, d, e, a, me2, 29); \ + if (t > 28) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(c, d, e, a, b, me2, 28); \ + if (t > 27) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(d, e, a, b, c, me2, 27); \ + if (t > 26) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(e, a, b, c, d, me2, 26); \ + if (t > 25) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(a, b, c, d, e, me2, 25); \ + if (t > 24) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(b, c, d, e, a, me2, 24); \ + if (t > 23) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(c, d, e, a, b, me2, 23); \ + if (t > 22) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(d, e, a, b, c, me2, 22); \ + if (t > 21) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(e, a, b, c, d, me2, 21); \ + if (t > 20) HASHCLASH_SHA1COMPRESS_ROUND2_STEP_BW(a, b, c, d, e, me2, 20); \ + if (t > 19) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(b, c, d, e, a, me2, 19); \ + if (t > 18) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(c, d, e, a, b, me2, 18); \ + if (t > 17) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(d, e, a, b, c, me2, 17); \ + if (t > 16) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(e, a, b, c, d, me2, 16); \ + if (t > 15) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(a, b, c, d, e, me2, 15); \ + if (t > 14) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(b, c, d, e, a, me2, 14); \ + if (t > 13) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(c, d, e, a, b, me2, 13); \ + if (t > 12) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(d, e, a, b, c, me2, 12); \ + if (t > 11) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(e, a, b, c, d, me2, 11); \ + if (t > 10) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(a, b, c, d, e, me2, 10); \ + if (t > 9) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(b, c, d, e, a, me2, 9); \ + if (t > 8) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(c, d, e, a, b, me2, 8); \ + if (t > 7) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(d, e, a, b, c, me2, 7); \ + if (t > 6) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(e, a, b, c, d, me2, 6); \ + if (t > 5) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(a, b, c, d, e, me2, 5); \ + if (t > 4) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(b, c, d, e, a, me2, 4); \ + if (t > 3) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(c, d, e, a, b, me2, 3); \ + if (t > 2) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(d, e, a, b, c, me2, 2); \ + if (t > 1) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(e, a, b, c, d, me2, 1); \ + if (t > 0) HASHCLASH_SHA1COMPRESS_ROUND1_STEP_BW(a, b, c, d, e, me2, 0); \ + ihvin[0] = a; ihvin[1] = b; ihvin[2] = c; ihvin[3] = d; ihvin[4] = e; \ + a = state[0]; b = state[1]; c = state[2]; d = state[3]; e = state[4]; \ + if (t <= 0) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, me2, 0); \ + if (t <= 1) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, me2, 1); \ + if (t <= 2) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, me2, 2); \ + if (t <= 3) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, me2, 3); \ + if (t <= 4) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, me2, 4); \ + if (t <= 5) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, me2, 5); \ + if (t <= 6) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, me2, 6); \ + if (t <= 7) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, me2, 7); \ + if (t <= 8) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, me2, 8); \ + if (t <= 9) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, me2, 9); \ + if (t <= 10) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, me2, 10); \ + if (t <= 11) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, me2, 11); \ + if (t <= 12) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, me2, 12); \ + if (t <= 13) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, me2, 13); \ + if (t <= 14) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, me2, 14); \ + if (t <= 15) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(a, b, c, d, e, me2, 15); \ + if (t <= 16) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(e, a, b, c, d, me2, 16); \ + if (t <= 17) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(d, e, a, b, c, me2, 17); \ + if (t <= 18) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(c, d, e, a, b, me2, 18); \ + if (t <= 19) HASHCLASH_SHA1COMPRESS_ROUND1_STEP(b, c, d, e, a, me2, 19); \ + if (t <= 20) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, me2, 20); \ + if (t <= 21) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, me2, 21); \ + if (t <= 22) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, me2, 22); \ + if (t <= 23) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, me2, 23); \ + if (t <= 24) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, me2, 24); \ + if (t <= 25) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, me2, 25); \ + if (t <= 26) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, me2, 26); \ + if (t <= 27) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, me2, 27); \ + if (t <= 28) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, me2, 28); \ + if (t <= 29) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, me2, 29); \ + if (t <= 30) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, me2, 30); \ + if (t <= 31) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, me2, 31); \ + if (t <= 32) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, me2, 32); \ + if (t <= 33) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, me2, 33); \ + if (t <= 34) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, me2, 34); \ + if (t <= 35) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(a, b, c, d, e, me2, 35); \ + if (t <= 36) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(e, a, b, c, d, me2, 36); \ + if (t <= 37) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(d, e, a, b, c, me2, 37); \ + if (t <= 38) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(c, d, e, a, b, me2, 38); \ + if (t <= 39) HASHCLASH_SHA1COMPRESS_ROUND2_STEP(b, c, d, e, a, me2, 39); \ + if (t <= 40) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, me2, 40); \ + if (t <= 41) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, me2, 41); \ + if (t <= 42) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, me2, 42); \ + if (t <= 43) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, me2, 43); \ + if (t <= 44) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, me2, 44); \ + if (t <= 45) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, me2, 45); \ + if (t <= 46) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, me2, 46); \ + if (t <= 47) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, me2, 47); \ + if (t <= 48) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, me2, 48); \ + if (t <= 49) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, me2, 49); \ + if (t <= 50) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, me2, 50); \ + if (t <= 51) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, me2, 51); \ + if (t <= 52) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, me2, 52); \ + if (t <= 53) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, me2, 53); \ + if (t <= 54) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, me2, 54); \ + if (t <= 55) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(a, b, c, d, e, me2, 55); \ + if (t <= 56) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(e, a, b, c, d, me2, 56); \ + if (t <= 57) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(d, e, a, b, c, me2, 57); \ + if (t <= 58) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(c, d, e, a, b, me2, 58); \ + if (t <= 59) HASHCLASH_SHA1COMPRESS_ROUND3_STEP(b, c, d, e, a, me2, 59); \ + if (t <= 60) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, me2, 60); \ + if (t <= 61) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, me2, 61); \ + if (t <= 62) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, me2, 62); \ + if (t <= 63) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, me2, 63); \ + if (t <= 64) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, me2, 64); \ + if (t <= 65) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, me2, 65); \ + if (t <= 66) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, me2, 66); \ + if (t <= 67) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, me2, 67); \ + if (t <= 68) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, me2, 68); \ + if (t <= 69) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, me2, 69); \ + if (t <= 70) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, me2, 70); \ + if (t <= 71) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, me2, 71); \ + if (t <= 72) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, me2, 72); \ + if (t <= 73) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, me2, 73); \ + if (t <= 74) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, me2, 74); \ + if (t <= 75) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(a, b, c, d, e, me2, 75); \ + if (t <= 76) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(e, a, b, c, d, me2, 76); \ + if (t <= 77) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(d, e, a, b, c, me2, 77); \ + if (t <= 78) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(c, d, e, a, b, me2, 78); \ + if (t <= 79) HASHCLASH_SHA1COMPRESS_ROUND4_STEP(b, c, d, e, a, me2, 79); \ + ihvout[0] = ihvin[0] + a; ihvout[1] = ihvin[1] + b; ihvout[2] = ihvin[2] + c; ihvout[3] = ihvin[3] + d; ihvout[4] = ihvin[4] + e; \ +} + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4127) /* Compiler complains about the checks in the above macro being constant. */ +#endif + +#ifdef DOSTORESTATE0 +SHA1_RECOMPRESS(0) +#endif + +#ifdef DOSTORESTATE1 +SHA1_RECOMPRESS(1) +#endif + +#ifdef DOSTORESTATE2 +SHA1_RECOMPRESS(2) +#endif + +#ifdef DOSTORESTATE3 +SHA1_RECOMPRESS(3) +#endif + +#ifdef DOSTORESTATE4 +SHA1_RECOMPRESS(4) +#endif + +#ifdef DOSTORESTATE5 +SHA1_RECOMPRESS(5) +#endif + +#ifdef DOSTORESTATE6 +SHA1_RECOMPRESS(6) +#endif + +#ifdef DOSTORESTATE7 +SHA1_RECOMPRESS(7) +#endif + +#ifdef DOSTORESTATE8 +SHA1_RECOMPRESS(8) +#endif + +#ifdef DOSTORESTATE9 +SHA1_RECOMPRESS(9) +#endif + +#ifdef DOSTORESTATE10 +SHA1_RECOMPRESS(10) +#endif + +#ifdef DOSTORESTATE11 +SHA1_RECOMPRESS(11) +#endif + +#ifdef DOSTORESTATE12 +SHA1_RECOMPRESS(12) +#endif + +#ifdef DOSTORESTATE13 +SHA1_RECOMPRESS(13) +#endif + +#ifdef DOSTORESTATE14 +SHA1_RECOMPRESS(14) +#endif + +#ifdef DOSTORESTATE15 +SHA1_RECOMPRESS(15) +#endif + +#ifdef DOSTORESTATE16 +SHA1_RECOMPRESS(16) +#endif + +#ifdef DOSTORESTATE17 +SHA1_RECOMPRESS(17) +#endif + +#ifdef DOSTORESTATE18 +SHA1_RECOMPRESS(18) +#endif + +#ifdef DOSTORESTATE19 +SHA1_RECOMPRESS(19) +#endif + +#ifdef DOSTORESTATE20 +SHA1_RECOMPRESS(20) +#endif + +#ifdef DOSTORESTATE21 +SHA1_RECOMPRESS(21) +#endif + +#ifdef DOSTORESTATE22 +SHA1_RECOMPRESS(22) +#endif + +#ifdef DOSTORESTATE23 +SHA1_RECOMPRESS(23) +#endif + +#ifdef DOSTORESTATE24 +SHA1_RECOMPRESS(24) +#endif + +#ifdef DOSTORESTATE25 +SHA1_RECOMPRESS(25) +#endif + +#ifdef DOSTORESTATE26 +SHA1_RECOMPRESS(26) +#endif + +#ifdef DOSTORESTATE27 +SHA1_RECOMPRESS(27) +#endif + +#ifdef DOSTORESTATE28 +SHA1_RECOMPRESS(28) +#endif + +#ifdef DOSTORESTATE29 +SHA1_RECOMPRESS(29) +#endif + +#ifdef DOSTORESTATE30 +SHA1_RECOMPRESS(30) +#endif + +#ifdef DOSTORESTATE31 +SHA1_RECOMPRESS(31) +#endif + +#ifdef DOSTORESTATE32 +SHA1_RECOMPRESS(32) +#endif + +#ifdef DOSTORESTATE33 +SHA1_RECOMPRESS(33) +#endif + +#ifdef DOSTORESTATE34 +SHA1_RECOMPRESS(34) +#endif + +#ifdef DOSTORESTATE35 +SHA1_RECOMPRESS(35) +#endif + +#ifdef DOSTORESTATE36 +SHA1_RECOMPRESS(36) +#endif + +#ifdef DOSTORESTATE37 +SHA1_RECOMPRESS(37) +#endif + +#ifdef DOSTORESTATE38 +SHA1_RECOMPRESS(38) +#endif + +#ifdef DOSTORESTATE39 +SHA1_RECOMPRESS(39) +#endif + +#ifdef DOSTORESTATE40 +SHA1_RECOMPRESS(40) +#endif + +#ifdef DOSTORESTATE41 +SHA1_RECOMPRESS(41) +#endif + +#ifdef DOSTORESTATE42 +SHA1_RECOMPRESS(42) +#endif + +#ifdef DOSTORESTATE43 +SHA1_RECOMPRESS(43) +#endif + +#ifdef DOSTORESTATE44 +SHA1_RECOMPRESS(44) +#endif + +#ifdef DOSTORESTATE45 +SHA1_RECOMPRESS(45) +#endif + +#ifdef DOSTORESTATE46 +SHA1_RECOMPRESS(46) +#endif + +#ifdef DOSTORESTATE47 +SHA1_RECOMPRESS(47) +#endif + +#ifdef DOSTORESTATE48 +SHA1_RECOMPRESS(48) +#endif + +#ifdef DOSTORESTATE49 +SHA1_RECOMPRESS(49) +#endif + +#ifdef DOSTORESTATE50 +SHA1_RECOMPRESS(50) +#endif + +#ifdef DOSTORESTATE51 +SHA1_RECOMPRESS(51) +#endif + +#ifdef DOSTORESTATE52 +SHA1_RECOMPRESS(52) +#endif + +#ifdef DOSTORESTATE53 +SHA1_RECOMPRESS(53) +#endif + +#ifdef DOSTORESTATE54 +SHA1_RECOMPRESS(54) +#endif + +#ifdef DOSTORESTATE55 +SHA1_RECOMPRESS(55) +#endif + +#ifdef DOSTORESTATE56 +SHA1_RECOMPRESS(56) +#endif + +#ifdef DOSTORESTATE57 +SHA1_RECOMPRESS(57) +#endif + +#ifdef DOSTORESTATE58 +SHA1_RECOMPRESS(58) +#endif + +#ifdef DOSTORESTATE59 +SHA1_RECOMPRESS(59) +#endif + +#ifdef DOSTORESTATE60 +SHA1_RECOMPRESS(60) +#endif + +#ifdef DOSTORESTATE61 +SHA1_RECOMPRESS(61) +#endif + +#ifdef DOSTORESTATE62 +SHA1_RECOMPRESS(62) +#endif + +#ifdef DOSTORESTATE63 +SHA1_RECOMPRESS(63) +#endif + +#ifdef DOSTORESTATE64 +SHA1_RECOMPRESS(64) +#endif + +#ifdef DOSTORESTATE65 +SHA1_RECOMPRESS(65) +#endif + +#ifdef DOSTORESTATE66 +SHA1_RECOMPRESS(66) +#endif + +#ifdef DOSTORESTATE67 +SHA1_RECOMPRESS(67) +#endif + +#ifdef DOSTORESTATE68 +SHA1_RECOMPRESS(68) +#endif + +#ifdef DOSTORESTATE69 +SHA1_RECOMPRESS(69) +#endif + +#ifdef DOSTORESTATE70 +SHA1_RECOMPRESS(70) +#endif + +#ifdef DOSTORESTATE71 +SHA1_RECOMPRESS(71) +#endif + +#ifdef DOSTORESTATE72 +SHA1_RECOMPRESS(72) +#endif + +#ifdef DOSTORESTATE73 +SHA1_RECOMPRESS(73) +#endif + +#ifdef DOSTORESTATE74 +SHA1_RECOMPRESS(74) +#endif + +#ifdef DOSTORESTATE75 +SHA1_RECOMPRESS(75) +#endif + +#ifdef DOSTORESTATE76 +SHA1_RECOMPRESS(76) +#endif + +#ifdef DOSTORESTATE77 +SHA1_RECOMPRESS(77) +#endif + +#ifdef DOSTORESTATE78 +SHA1_RECOMPRESS(78) +#endif + +#ifdef DOSTORESTATE79 +SHA1_RECOMPRESS(79) +#endif + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +static void sha1_recompression_step(uint32_t step, uint32_t ihvin[5], uint32_t ihvout[5], const uint32_t me2[80], const uint32_t state[5]) +{ + switch (step) + { +#ifdef DOSTORESTATE0 + case 0: + sha1recompress_fast_0(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE1 + case 1: + sha1recompress_fast_1(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE2 + case 2: + sha1recompress_fast_2(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE3 + case 3: + sha1recompress_fast_3(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE4 + case 4: + sha1recompress_fast_4(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE5 + case 5: + sha1recompress_fast_5(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE6 + case 6: + sha1recompress_fast_6(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE7 + case 7: + sha1recompress_fast_7(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE8 + case 8: + sha1recompress_fast_8(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE9 + case 9: + sha1recompress_fast_9(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE10 + case 10: + sha1recompress_fast_10(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE11 + case 11: + sha1recompress_fast_11(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE12 + case 12: + sha1recompress_fast_12(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE13 + case 13: + sha1recompress_fast_13(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE14 + case 14: + sha1recompress_fast_14(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE15 + case 15: + sha1recompress_fast_15(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE16 + case 16: + sha1recompress_fast_16(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE17 + case 17: + sha1recompress_fast_17(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE18 + case 18: + sha1recompress_fast_18(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE19 + case 19: + sha1recompress_fast_19(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE20 + case 20: + sha1recompress_fast_20(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE21 + case 21: + sha1recompress_fast_21(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE22 + case 22: + sha1recompress_fast_22(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE23 + case 23: + sha1recompress_fast_23(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE24 + case 24: + sha1recompress_fast_24(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE25 + case 25: + sha1recompress_fast_25(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE26 + case 26: + sha1recompress_fast_26(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE27 + case 27: + sha1recompress_fast_27(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE28 + case 28: + sha1recompress_fast_28(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE29 + case 29: + sha1recompress_fast_29(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE30 + case 30: + sha1recompress_fast_30(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE31 + case 31: + sha1recompress_fast_31(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE32 + case 32: + sha1recompress_fast_32(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE33 + case 33: + sha1recompress_fast_33(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE34 + case 34: + sha1recompress_fast_34(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE35 + case 35: + sha1recompress_fast_35(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE36 + case 36: + sha1recompress_fast_36(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE37 + case 37: + sha1recompress_fast_37(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE38 + case 38: + sha1recompress_fast_38(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE39 + case 39: + sha1recompress_fast_39(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE40 + case 40: + sha1recompress_fast_40(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE41 + case 41: + sha1recompress_fast_41(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE42 + case 42: + sha1recompress_fast_42(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE43 + case 43: + sha1recompress_fast_43(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE44 + case 44: + sha1recompress_fast_44(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE45 + case 45: + sha1recompress_fast_45(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE46 + case 46: + sha1recompress_fast_46(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE47 + case 47: + sha1recompress_fast_47(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE48 + case 48: + sha1recompress_fast_48(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE49 + case 49: + sha1recompress_fast_49(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE50 + case 50: + sha1recompress_fast_50(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE51 + case 51: + sha1recompress_fast_51(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE52 + case 52: + sha1recompress_fast_52(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE53 + case 53: + sha1recompress_fast_53(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE54 + case 54: + sha1recompress_fast_54(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE55 + case 55: + sha1recompress_fast_55(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE56 + case 56: + sha1recompress_fast_56(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE57 + case 57: + sha1recompress_fast_57(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE58 + case 58: + sha1recompress_fast_58(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE59 + case 59: + sha1recompress_fast_59(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE60 + case 60: + sha1recompress_fast_60(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE61 + case 61: + sha1recompress_fast_61(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE62 + case 62: + sha1recompress_fast_62(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE63 + case 63: + sha1recompress_fast_63(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE64 + case 64: + sha1recompress_fast_64(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE65 + case 65: + sha1recompress_fast_65(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE66 + case 66: + sha1recompress_fast_66(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE67 + case 67: + sha1recompress_fast_67(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE68 + case 68: + sha1recompress_fast_68(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE69 + case 69: + sha1recompress_fast_69(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE70 + case 70: + sha1recompress_fast_70(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE71 + case 71: + sha1recompress_fast_71(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE72 + case 72: + sha1recompress_fast_72(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE73 + case 73: + sha1recompress_fast_73(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE74 + case 74: + sha1recompress_fast_74(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE75 + case 75: + sha1recompress_fast_75(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE76 + case 76: + sha1recompress_fast_76(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE77 + case 77: + sha1recompress_fast_77(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE78 + case 78: + sha1recompress_fast_78(ihvin, ihvout, me2, state); + break; +#endif +#ifdef DOSTORESTATE79 + case 79: + sha1recompress_fast_79(ihvin, ihvout, me2, state); + break; +#endif + default: + abort(); + } + +} + + + +static void sha1_process(SHA1_CTX *ctx, const uint32_t block[16]) +{ + unsigned i, j; + uint32_t ubc_dv_mask[DVMASKSIZE] = { 0xFFFFFFFF }; + uint32_t ihvtmp[5]; + + ctx->ihv1[0] = ctx->ihv[0]; + ctx->ihv1[1] = ctx->ihv[1]; + ctx->ihv1[2] = ctx->ihv[2]; + ctx->ihv1[3] = ctx->ihv[3]; + ctx->ihv1[4] = ctx->ihv[4]; + + sha1_compression_states(ctx->ihv, block, ctx->m1, ctx->states); + + if (ctx->detect_coll) + { + if (ctx->ubc_check) + { + ubc_check(ctx->m1, ubc_dv_mask); + } + + if (ubc_dv_mask[0] != 0) + { + for (i = 0; sha1_dvs[i].dvType != 0; ++i) + { + if (ubc_dv_mask[0] & ((uint32_t)(1) << sha1_dvs[i].maskb)) + { + for (j = 0; j < 80; ++j) + ctx->m2[j] = ctx->m1[j] ^ sha1_dvs[i].dm[j]; + + sha1_recompression_step(sha1_dvs[i].testt, ctx->ihv2, ihvtmp, ctx->m2, ctx->states[sha1_dvs[i].testt]); + + /* to verify SHA-1 collision detection code with collisions for reduced-step SHA-1 */ + if ((0 == ((ihvtmp[0] ^ ctx->ihv[0]) | (ihvtmp[1] ^ ctx->ihv[1]) | (ihvtmp[2] ^ ctx->ihv[2]) | (ihvtmp[3] ^ ctx->ihv[3]) | (ihvtmp[4] ^ ctx->ihv[4]))) + || (ctx->reduced_round_coll && 0==((ctx->ihv1[0] ^ ctx->ihv2[0]) | (ctx->ihv1[1] ^ ctx->ihv2[1]) | (ctx->ihv1[2] ^ ctx->ihv2[2]) | (ctx->ihv1[3] ^ ctx->ihv2[3]) | (ctx->ihv1[4] ^ ctx->ihv2[4])))) + { + ctx->found_collision = 1; + + if (ctx->safe_hash) + { + sha1_compression_W(ctx->ihv, ctx->m1); + sha1_compression_W(ctx->ihv, ctx->m1); + } + + break; + } + } + } + } + } +} + +void SHA1DCInit(SHA1_CTX *ctx) +{ + ctx->total = 0; + ctx->ihv[0] = 0x67452301; + ctx->ihv[1] = 0xEFCDAB89; + ctx->ihv[2] = 0x98BADCFE; + ctx->ihv[3] = 0x10325476; + ctx->ihv[4] = 0xC3D2E1F0; + ctx->found_collision = 0; + ctx->safe_hash = SHA1DC_INIT_SAFE_HASH_DEFAULT; + ctx->ubc_check = 1; + ctx->detect_coll = 1; + ctx->reduced_round_coll = 0; + ctx->callback = NULL; +} + +void SHA1DCSetSafeHash(SHA1_CTX *ctx, int safehash) +{ + if (safehash) + ctx->safe_hash = 1; + else + ctx->safe_hash = 0; +} + + +void SHA1DCSetUseUBC(SHA1_CTX *ctx, int ubc_check) +{ + if (ubc_check) + ctx->ubc_check = 1; + else + ctx->ubc_check = 0; +} + +void SHA1DCSetUseDetectColl(SHA1_CTX *ctx, int detect_coll) +{ + if (detect_coll) + ctx->detect_coll = 1; + else + ctx->detect_coll = 0; +} + +void SHA1DCSetDetectReducedRoundCollision(SHA1_CTX *ctx, int reduced_round_coll) +{ + if (reduced_round_coll) + ctx->reduced_round_coll = 1; + else + ctx->reduced_round_coll = 0; +} + +void SHA1DCSetCallback(SHA1_CTX *ctx, collision_block_callback callback) +{ + ctx->callback = callback; +} + +void SHA1DCUpdate(SHA1_CTX *ctx, const char *buf, size_t len) +{ + unsigned left, fill; + + if (len == 0) + return; + + left = ctx->total & 63; + fill = 64 - left; + + if (left && len >= fill) + { + ctx->total += fill; + memcpy(ctx->buffer + left, buf, fill); + sha1_process(ctx, (uint32_t*)(ctx->buffer)); + buf += fill; + len -= fill; + left = 0; + } + while (len >= 64) + { + ctx->total += 64; + +#if defined(SHA1DC_ALLOW_UNALIGNED_ACCESS) + sha1_process(ctx, (uint32_t*)(buf)); +#else + memcpy(ctx->buffer, buf, 64); + sha1_process(ctx, (uint32_t*)(ctx->buffer)); +#endif /* defined(SHA1DC_ALLOW_UNALIGNED_ACCESS) */ + buf += 64; + len -= 64; + } + if (len > 0) + { + ctx->total += len; + memcpy(ctx->buffer + left, buf, len); + } +} + +static const unsigned char sha1_padding[64] = +{ + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +int SHA1DCFinal(unsigned char output[20], SHA1_CTX *ctx) +{ + uint32_t last = ctx->total & 63; + uint32_t padn = (last < 56) ? (56 - last) : (120 - last); + uint64_t total; + SHA1DCUpdate(ctx, (const char*)(sha1_padding), padn); + + total = ctx->total - padn; + total <<= 3; + ctx->buffer[56] = (unsigned char)(total >> 56); + ctx->buffer[57] = (unsigned char)(total >> 48); + ctx->buffer[58] = (unsigned char)(total >> 40); + ctx->buffer[59] = (unsigned char)(total >> 32); + ctx->buffer[60] = (unsigned char)(total >> 24); + ctx->buffer[61] = (unsigned char)(total >> 16); + ctx->buffer[62] = (unsigned char)(total >> 8); + ctx->buffer[63] = (unsigned char)(total); + sha1_process(ctx, (uint32_t*)(ctx->buffer)); + output[0] = (unsigned char)(ctx->ihv[0] >> 24); + output[1] = (unsigned char)(ctx->ihv[0] >> 16); + output[2] = (unsigned char)(ctx->ihv[0] >> 8); + output[3] = (unsigned char)(ctx->ihv[0]); + output[4] = (unsigned char)(ctx->ihv[1] >> 24); + output[5] = (unsigned char)(ctx->ihv[1] >> 16); + output[6] = (unsigned char)(ctx->ihv[1] >> 8); + output[7] = (unsigned char)(ctx->ihv[1]); + output[8] = (unsigned char)(ctx->ihv[2] >> 24); + output[9] = (unsigned char)(ctx->ihv[2] >> 16); + output[10] = (unsigned char)(ctx->ihv[2] >> 8); + output[11] = (unsigned char)(ctx->ihv[2]); + output[12] = (unsigned char)(ctx->ihv[3] >> 24); + output[13] = (unsigned char)(ctx->ihv[3] >> 16); + output[14] = (unsigned char)(ctx->ihv[3] >> 8); + output[15] = (unsigned char)(ctx->ihv[3]); + output[16] = (unsigned char)(ctx->ihv[4] >> 24); + output[17] = (unsigned char)(ctx->ihv[4] >> 16); + output[18] = (unsigned char)(ctx->ihv[4] >> 8); + output[19] = (unsigned char)(ctx->ihv[4]); + return ctx->found_collision; +} + +#ifdef SHA1DC_CUSTOM_TRAILING_INCLUDE_SHA1_C +#include SHA1DC_CUSTOM_TRAILING_INCLUDE_SHA1_C +#endif diff --git a/src/util/hash/sha1dc/sha1.h b/src/util/hash/sha1dc/sha1.h new file mode 100644 index 0000000..1e4e94b --- /dev/null +++ b/src/util/hash/sha1dc/sha1.h @@ -0,0 +1,110 @@ +/*** +* Copyright 2017 Marc Stevens , Dan Shumow +* Distributed under the MIT Software License. +* See accompanying file LICENSE.txt or copy at +* https://opensource.org/licenses/MIT +***/ + +#ifndef SHA1DC_SHA1_H +#define SHA1DC_SHA1_H + +#if defined(__cplusplus) +extern "C" { +#endif + +#ifndef SHA1DC_NO_STANDARD_INCLUDES +#include +#endif + +/* sha-1 compression function that takes an already expanded message, and additionally store intermediate states */ +/* only stores states ii (the state between step ii-1 and step ii) when DOSTORESTATEii is defined in ubc_check.h */ +void sha1_compression_states(uint32_t[5], const uint32_t[16], uint32_t[80], uint32_t[80][5]); + +/* +// Function type for sha1_recompression_step_T (uint32_t ihvin[5], uint32_t ihvout[5], const uint32_t me2[80], const uint32_t state[5]). +// Where 0 <= T < 80 +// me2 is an expanded message (the expansion of an original message block XOR'ed with a disturbance vector's message block difference.) +// state is the internal state (a,b,c,d,e) before step T of the SHA-1 compression function while processing the original message block. +// The function will return: +// ihvin: The reconstructed input chaining value. +// ihvout: The reconstructed output chaining value. +*/ +typedef void(*sha1_recompression_type)(uint32_t*, uint32_t*, const uint32_t*, const uint32_t*); + +/* A callback function type that can be set to be called when a collision block has been found: */ +/* void collision_block_callback(uint64_t byteoffset, const uint32_t ihvin1[5], const uint32_t ihvin2[5], const uint32_t m1[80], const uint32_t m2[80]) */ +typedef void(*collision_block_callback)(uint64_t, const uint32_t*, const uint32_t*, const uint32_t*, const uint32_t*); + +/* The SHA-1 context. */ +typedef struct { + uint64_t total; + uint32_t ihv[5]; + unsigned char buffer[64]; + int found_collision; + int safe_hash; + int detect_coll; + int ubc_check; + int reduced_round_coll; + collision_block_callback callback; + + uint32_t ihv1[5]; + uint32_t ihv2[5]; + uint32_t m1[80]; + uint32_t m2[80]; + uint32_t states[80][5]; +} SHA1_CTX; + +/* Initialize SHA-1 context. */ +void SHA1DCInit(SHA1_CTX*); + +/* + Function to enable safe SHA-1 hashing: + Collision attacks are thwarted by hashing a detected near-collision block 3 times. + Think of it as extending SHA-1 from 80-steps to 240-steps for such blocks: + The best collision attacks against SHA-1 have complexity about 2^60, + thus for 240-steps an immediate lower-bound for the best cryptanalytic attacks would be 2^180. + An attacker would be better off using a generic birthday search of complexity 2^80. + + Enabling safe SHA-1 hashing will result in the correct SHA-1 hash for messages where no collision attack was detected, + but it will result in a different SHA-1 hash for messages where a collision attack was detected. + This will automatically invalidate SHA-1 based digital signature forgeries. + Enabled by default. +*/ +void SHA1DCSetSafeHash(SHA1_CTX*, int); + +/* + Function to disable or enable the use of Unavoidable Bitconditions (provides a significant speed up). + Enabled by default + */ +void SHA1DCSetUseUBC(SHA1_CTX*, int); + +/* + Function to disable or enable the use of Collision Detection. + Enabled by default. + */ +void SHA1DCSetUseDetectColl(SHA1_CTX*, int); + +/* function to disable or enable the detection of reduced-round SHA-1 collisions */ +/* disabled by default */ +void SHA1DCSetDetectReducedRoundCollision(SHA1_CTX*, int); + +/* function to set a callback function, pass NULL to disable */ +/* by default no callback set */ +void SHA1DCSetCallback(SHA1_CTX*, collision_block_callback); + +/* update SHA-1 context with buffer contents */ +void SHA1DCUpdate(SHA1_CTX*, const char*, size_t); + +/* obtain SHA-1 hash from SHA-1 context */ +/* returns: 0 = no collision detected, otherwise = collision found => warn user for active attack */ +int SHA1DCFinal(unsigned char[20], SHA1_CTX*); + +#if defined(__cplusplus) +} +#endif + +#ifdef SHA1DC_CUSTOM_TRAILING_INCLUDE_SHA1_H +#include SHA1DC_CUSTOM_TRAILING_INCLUDE_SHA1_H +#endif + +#endif diff --git a/src/util/hash/sha1dc/ubc_check.c b/src/util/hash/sha1dc/ubc_check.c new file mode 100644 index 0000000..b3beff2 --- /dev/null +++ b/src/util/hash/sha1dc/ubc_check.c @@ -0,0 +1,372 @@ +/*** +* Copyright 2017 Marc Stevens , Dan Shumow +* Distributed under the MIT Software License. +* See accompanying file LICENSE.txt or copy at +* https://opensource.org/licenses/MIT +***/ + +/* +// this file was generated by the 'parse_bitrel' program in the tools section +// using the data files from directory 'tools/data/3565' +// +// sha1_dvs contains a list of SHA-1 Disturbance Vectors (DV) to check +// dvType, dvK and dvB define the DV: I(K,B) or II(K,B) (see the paper) +// dm[80] is the expanded message block XOR-difference defined by the DV +// testt is the step to do the recompression from for collision detection +// maski and maskb define the bit to check for each DV in the dvmask returned by ubc_check +// +// ubc_check takes as input an expanded message block and verifies the unavoidable bitconditions for all listed DVs +// it returns a dvmask where each bit belonging to a DV is set if all unavoidable bitconditions for that DV have been met +// thus one needs to do the recompression check for each DV that has its bit set +// +// ubc_check is programmatically generated and the unavoidable bitconditions have been hardcoded +// a directly verifiable version named ubc_check_verify can be found in ubc_check_verify.c +// ubc_check has been verified against ubc_check_verify using the 'ubc_check_test' program in the tools section +*/ + +#ifndef SHA1DC_NO_STANDARD_INCLUDES +#include +#endif +#ifdef SHA1DC_CUSTOM_INCLUDE_UBC_CHECK_C +#include SHA1DC_CUSTOM_INCLUDE_UBC_CHECK_C +#endif +#include "ubc_check.h" + +static const uint32_t DV_I_43_0_bit = (uint32_t)(1) << 0; +static const uint32_t DV_I_44_0_bit = (uint32_t)(1) << 1; +static const uint32_t DV_I_45_0_bit = (uint32_t)(1) << 2; +static const uint32_t DV_I_46_0_bit = (uint32_t)(1) << 3; +static const uint32_t DV_I_46_2_bit = (uint32_t)(1) << 4; +static const uint32_t DV_I_47_0_bit = (uint32_t)(1) << 5; +static const uint32_t DV_I_47_2_bit = (uint32_t)(1) << 6; +static const uint32_t DV_I_48_0_bit = (uint32_t)(1) << 7; +static const uint32_t DV_I_48_2_bit = (uint32_t)(1) << 8; +static const uint32_t DV_I_49_0_bit = (uint32_t)(1) << 9; +static const uint32_t DV_I_49_2_bit = (uint32_t)(1) << 10; +static const uint32_t DV_I_50_0_bit = (uint32_t)(1) << 11; +static const uint32_t DV_I_50_2_bit = (uint32_t)(1) << 12; +static const uint32_t DV_I_51_0_bit = (uint32_t)(1) << 13; +static const uint32_t DV_I_51_2_bit = (uint32_t)(1) << 14; +static const uint32_t DV_I_52_0_bit = (uint32_t)(1) << 15; +static const uint32_t DV_II_45_0_bit = (uint32_t)(1) << 16; +static const uint32_t DV_II_46_0_bit = (uint32_t)(1) << 17; +static const uint32_t DV_II_46_2_bit = (uint32_t)(1) << 18; +static const uint32_t DV_II_47_0_bit = (uint32_t)(1) << 19; +static const uint32_t DV_II_48_0_bit = (uint32_t)(1) << 20; +static const uint32_t DV_II_49_0_bit = (uint32_t)(1) << 21; +static const uint32_t DV_II_49_2_bit = (uint32_t)(1) << 22; +static const uint32_t DV_II_50_0_bit = (uint32_t)(1) << 23; +static const uint32_t DV_II_50_2_bit = (uint32_t)(1) << 24; +static const uint32_t DV_II_51_0_bit = (uint32_t)(1) << 25; +static const uint32_t DV_II_51_2_bit = (uint32_t)(1) << 26; +static const uint32_t DV_II_52_0_bit = (uint32_t)(1) << 27; +static const uint32_t DV_II_53_0_bit = (uint32_t)(1) << 28; +static const uint32_t DV_II_54_0_bit = (uint32_t)(1) << 29; +static const uint32_t DV_II_55_0_bit = (uint32_t)(1) << 30; +static const uint32_t DV_II_56_0_bit = (uint32_t)(1) << 31; + +dv_info_t sha1_dvs[] = +{ + {1,43,0,58,0,0, { 0x08000000,0x9800000c,0xd8000010,0x08000010,0xb8000010,0x98000000,0x60000000,0x00000008,0xc0000000,0x90000014,0x10000010,0xb8000014,0x28000000,0x20000010,0x48000000,0x08000018,0x60000000,0x90000010,0xf0000010,0x90000008,0xc0000000,0x90000010,0xf0000010,0xb0000008,0x40000000,0x90000000,0xf0000010,0x90000018,0x60000000,0x90000010,0x90000010,0x90000000,0x80000000,0x00000010,0xa0000000,0x20000000,0xa0000000,0x20000010,0x00000000,0x20000010,0x20000000,0x00000010,0x20000000,0x00000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000040,0x40000002,0x80000004,0x80000080,0x80000006,0x00000049,0x00000103,0x80000009,0x80000012,0x80000202,0x00000018,0x00000164,0x00000408,0x800000e6,0x8000004c,0x00000803,0x80000161,0x80000599 } } +, {1,44,0,58,0,1, { 0xb4000008,0x08000000,0x9800000c,0xd8000010,0x08000010,0xb8000010,0x98000000,0x60000000,0x00000008,0xc0000000,0x90000014,0x10000010,0xb8000014,0x28000000,0x20000010,0x48000000,0x08000018,0x60000000,0x90000010,0xf0000010,0x90000008,0xc0000000,0x90000010,0xf0000010,0xb0000008,0x40000000,0x90000000,0xf0000010,0x90000018,0x60000000,0x90000010,0x90000010,0x90000000,0x80000000,0x00000010,0xa0000000,0x20000000,0xa0000000,0x20000010,0x00000000,0x20000010,0x20000000,0x00000010,0x20000000,0x00000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000040,0x40000002,0x80000004,0x80000080,0x80000006,0x00000049,0x00000103,0x80000009,0x80000012,0x80000202,0x00000018,0x00000164,0x00000408,0x800000e6,0x8000004c,0x00000803,0x80000161 } } +, {1,45,0,58,0,2, { 0xf4000014,0xb4000008,0x08000000,0x9800000c,0xd8000010,0x08000010,0xb8000010,0x98000000,0x60000000,0x00000008,0xc0000000,0x90000014,0x10000010,0xb8000014,0x28000000,0x20000010,0x48000000,0x08000018,0x60000000,0x90000010,0xf0000010,0x90000008,0xc0000000,0x90000010,0xf0000010,0xb0000008,0x40000000,0x90000000,0xf0000010,0x90000018,0x60000000,0x90000010,0x90000010,0x90000000,0x80000000,0x00000010,0xa0000000,0x20000000,0xa0000000,0x20000010,0x00000000,0x20000010,0x20000000,0x00000010,0x20000000,0x00000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000040,0x40000002,0x80000004,0x80000080,0x80000006,0x00000049,0x00000103,0x80000009,0x80000012,0x80000202,0x00000018,0x00000164,0x00000408,0x800000e6,0x8000004c,0x00000803 } } +, {1,46,0,58,0,3, { 0x2c000010,0xf4000014,0xb4000008,0x08000000,0x9800000c,0xd8000010,0x08000010,0xb8000010,0x98000000,0x60000000,0x00000008,0xc0000000,0x90000014,0x10000010,0xb8000014,0x28000000,0x20000010,0x48000000,0x08000018,0x60000000,0x90000010,0xf0000010,0x90000008,0xc0000000,0x90000010,0xf0000010,0xb0000008,0x40000000,0x90000000,0xf0000010,0x90000018,0x60000000,0x90000010,0x90000010,0x90000000,0x80000000,0x00000010,0xa0000000,0x20000000,0xa0000000,0x20000010,0x00000000,0x20000010,0x20000000,0x00000010,0x20000000,0x00000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000040,0x40000002,0x80000004,0x80000080,0x80000006,0x00000049,0x00000103,0x80000009,0x80000012,0x80000202,0x00000018,0x00000164,0x00000408,0x800000e6,0x8000004c } } +, {1,46,2,58,0,4, { 0xb0000040,0xd0000053,0xd0000022,0x20000000,0x60000032,0x60000043,0x20000040,0xe0000042,0x60000002,0x80000001,0x00000020,0x00000003,0x40000052,0x40000040,0xe0000052,0xa0000000,0x80000040,0x20000001,0x20000060,0x80000001,0x40000042,0xc0000043,0x40000022,0x00000003,0x40000042,0xc0000043,0xc0000022,0x00000001,0x40000002,0xc0000043,0x40000062,0x80000001,0x40000042,0x40000042,0x40000002,0x00000002,0x00000040,0x80000002,0x80000000,0x80000002,0x80000040,0x00000000,0x80000040,0x80000000,0x00000040,0x80000000,0x00000040,0x80000002,0x00000000,0x80000000,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000004,0x00000080,0x00000004,0x00000009,0x00000101,0x00000009,0x00000012,0x00000202,0x0000001a,0x00000124,0x0000040c,0x00000026,0x0000004a,0x0000080a,0x00000060,0x00000590,0x00001020,0x0000039a,0x00000132 } } +, {1,47,0,58,0,5, { 0xc8000010,0x2c000010,0xf4000014,0xb4000008,0x08000000,0x9800000c,0xd8000010,0x08000010,0xb8000010,0x98000000,0x60000000,0x00000008,0xc0000000,0x90000014,0x10000010,0xb8000014,0x28000000,0x20000010,0x48000000,0x08000018,0x60000000,0x90000010,0xf0000010,0x90000008,0xc0000000,0x90000010,0xf0000010,0xb0000008,0x40000000,0x90000000,0xf0000010,0x90000018,0x60000000,0x90000010,0x90000010,0x90000000,0x80000000,0x00000010,0xa0000000,0x20000000,0xa0000000,0x20000010,0x00000000,0x20000010,0x20000000,0x00000010,0x20000000,0x00000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000040,0x40000002,0x80000004,0x80000080,0x80000006,0x00000049,0x00000103,0x80000009,0x80000012,0x80000202,0x00000018,0x00000164,0x00000408,0x800000e6 } } +, {1,47,2,58,0,6, { 0x20000043,0xb0000040,0xd0000053,0xd0000022,0x20000000,0x60000032,0x60000043,0x20000040,0xe0000042,0x60000002,0x80000001,0x00000020,0x00000003,0x40000052,0x40000040,0xe0000052,0xa0000000,0x80000040,0x20000001,0x20000060,0x80000001,0x40000042,0xc0000043,0x40000022,0x00000003,0x40000042,0xc0000043,0xc0000022,0x00000001,0x40000002,0xc0000043,0x40000062,0x80000001,0x40000042,0x40000042,0x40000002,0x00000002,0x00000040,0x80000002,0x80000000,0x80000002,0x80000040,0x00000000,0x80000040,0x80000000,0x00000040,0x80000000,0x00000040,0x80000002,0x00000000,0x80000000,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000004,0x00000080,0x00000004,0x00000009,0x00000101,0x00000009,0x00000012,0x00000202,0x0000001a,0x00000124,0x0000040c,0x00000026,0x0000004a,0x0000080a,0x00000060,0x00000590,0x00001020,0x0000039a } } +, {1,48,0,58,0,7, { 0xb800000a,0xc8000010,0x2c000010,0xf4000014,0xb4000008,0x08000000,0x9800000c,0xd8000010,0x08000010,0xb8000010,0x98000000,0x60000000,0x00000008,0xc0000000,0x90000014,0x10000010,0xb8000014,0x28000000,0x20000010,0x48000000,0x08000018,0x60000000,0x90000010,0xf0000010,0x90000008,0xc0000000,0x90000010,0xf0000010,0xb0000008,0x40000000,0x90000000,0xf0000010,0x90000018,0x60000000,0x90000010,0x90000010,0x90000000,0x80000000,0x00000010,0xa0000000,0x20000000,0xa0000000,0x20000010,0x00000000,0x20000010,0x20000000,0x00000010,0x20000000,0x00000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000040,0x40000002,0x80000004,0x80000080,0x80000006,0x00000049,0x00000103,0x80000009,0x80000012,0x80000202,0x00000018,0x00000164,0x00000408 } } +, {1,48,2,58,0,8, { 0xe000002a,0x20000043,0xb0000040,0xd0000053,0xd0000022,0x20000000,0x60000032,0x60000043,0x20000040,0xe0000042,0x60000002,0x80000001,0x00000020,0x00000003,0x40000052,0x40000040,0xe0000052,0xa0000000,0x80000040,0x20000001,0x20000060,0x80000001,0x40000042,0xc0000043,0x40000022,0x00000003,0x40000042,0xc0000043,0xc0000022,0x00000001,0x40000002,0xc0000043,0x40000062,0x80000001,0x40000042,0x40000042,0x40000002,0x00000002,0x00000040,0x80000002,0x80000000,0x80000002,0x80000040,0x00000000,0x80000040,0x80000000,0x00000040,0x80000000,0x00000040,0x80000002,0x00000000,0x80000000,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000004,0x00000080,0x00000004,0x00000009,0x00000101,0x00000009,0x00000012,0x00000202,0x0000001a,0x00000124,0x0000040c,0x00000026,0x0000004a,0x0000080a,0x00000060,0x00000590,0x00001020 } } +, {1,49,0,58,0,9, { 0x18000000,0xb800000a,0xc8000010,0x2c000010,0xf4000014,0xb4000008,0x08000000,0x9800000c,0xd8000010,0x08000010,0xb8000010,0x98000000,0x60000000,0x00000008,0xc0000000,0x90000014,0x10000010,0xb8000014,0x28000000,0x20000010,0x48000000,0x08000018,0x60000000,0x90000010,0xf0000010,0x90000008,0xc0000000,0x90000010,0xf0000010,0xb0000008,0x40000000,0x90000000,0xf0000010,0x90000018,0x60000000,0x90000010,0x90000010,0x90000000,0x80000000,0x00000010,0xa0000000,0x20000000,0xa0000000,0x20000010,0x00000000,0x20000010,0x20000000,0x00000010,0x20000000,0x00000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000040,0x40000002,0x80000004,0x80000080,0x80000006,0x00000049,0x00000103,0x80000009,0x80000012,0x80000202,0x00000018,0x00000164 } } +, {1,49,2,58,0,10, { 0x60000000,0xe000002a,0x20000043,0xb0000040,0xd0000053,0xd0000022,0x20000000,0x60000032,0x60000043,0x20000040,0xe0000042,0x60000002,0x80000001,0x00000020,0x00000003,0x40000052,0x40000040,0xe0000052,0xa0000000,0x80000040,0x20000001,0x20000060,0x80000001,0x40000042,0xc0000043,0x40000022,0x00000003,0x40000042,0xc0000043,0xc0000022,0x00000001,0x40000002,0xc0000043,0x40000062,0x80000001,0x40000042,0x40000042,0x40000002,0x00000002,0x00000040,0x80000002,0x80000000,0x80000002,0x80000040,0x00000000,0x80000040,0x80000000,0x00000040,0x80000000,0x00000040,0x80000002,0x00000000,0x80000000,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000004,0x00000080,0x00000004,0x00000009,0x00000101,0x00000009,0x00000012,0x00000202,0x0000001a,0x00000124,0x0000040c,0x00000026,0x0000004a,0x0000080a,0x00000060,0x00000590 } } +, {1,50,0,65,0,11, { 0x0800000c,0x18000000,0xb800000a,0xc8000010,0x2c000010,0xf4000014,0xb4000008,0x08000000,0x9800000c,0xd8000010,0x08000010,0xb8000010,0x98000000,0x60000000,0x00000008,0xc0000000,0x90000014,0x10000010,0xb8000014,0x28000000,0x20000010,0x48000000,0x08000018,0x60000000,0x90000010,0xf0000010,0x90000008,0xc0000000,0x90000010,0xf0000010,0xb0000008,0x40000000,0x90000000,0xf0000010,0x90000018,0x60000000,0x90000010,0x90000010,0x90000000,0x80000000,0x00000010,0xa0000000,0x20000000,0xa0000000,0x20000010,0x00000000,0x20000010,0x20000000,0x00000010,0x20000000,0x00000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000040,0x40000002,0x80000004,0x80000080,0x80000006,0x00000049,0x00000103,0x80000009,0x80000012,0x80000202,0x00000018 } } +, {1,50,2,65,0,12, { 0x20000030,0x60000000,0xe000002a,0x20000043,0xb0000040,0xd0000053,0xd0000022,0x20000000,0x60000032,0x60000043,0x20000040,0xe0000042,0x60000002,0x80000001,0x00000020,0x00000003,0x40000052,0x40000040,0xe0000052,0xa0000000,0x80000040,0x20000001,0x20000060,0x80000001,0x40000042,0xc0000043,0x40000022,0x00000003,0x40000042,0xc0000043,0xc0000022,0x00000001,0x40000002,0xc0000043,0x40000062,0x80000001,0x40000042,0x40000042,0x40000002,0x00000002,0x00000040,0x80000002,0x80000000,0x80000002,0x80000040,0x00000000,0x80000040,0x80000000,0x00000040,0x80000000,0x00000040,0x80000002,0x00000000,0x80000000,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000004,0x00000080,0x00000004,0x00000009,0x00000101,0x00000009,0x00000012,0x00000202,0x0000001a,0x00000124,0x0000040c,0x00000026,0x0000004a,0x0000080a,0x00000060 } } +, {1,51,0,65,0,13, { 0xe8000000,0x0800000c,0x18000000,0xb800000a,0xc8000010,0x2c000010,0xf4000014,0xb4000008,0x08000000,0x9800000c,0xd8000010,0x08000010,0xb8000010,0x98000000,0x60000000,0x00000008,0xc0000000,0x90000014,0x10000010,0xb8000014,0x28000000,0x20000010,0x48000000,0x08000018,0x60000000,0x90000010,0xf0000010,0x90000008,0xc0000000,0x90000010,0xf0000010,0xb0000008,0x40000000,0x90000000,0xf0000010,0x90000018,0x60000000,0x90000010,0x90000010,0x90000000,0x80000000,0x00000010,0xa0000000,0x20000000,0xa0000000,0x20000010,0x00000000,0x20000010,0x20000000,0x00000010,0x20000000,0x00000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000040,0x40000002,0x80000004,0x80000080,0x80000006,0x00000049,0x00000103,0x80000009,0x80000012,0x80000202 } } +, {1,51,2,65,0,14, { 0xa0000003,0x20000030,0x60000000,0xe000002a,0x20000043,0xb0000040,0xd0000053,0xd0000022,0x20000000,0x60000032,0x60000043,0x20000040,0xe0000042,0x60000002,0x80000001,0x00000020,0x00000003,0x40000052,0x40000040,0xe0000052,0xa0000000,0x80000040,0x20000001,0x20000060,0x80000001,0x40000042,0xc0000043,0x40000022,0x00000003,0x40000042,0xc0000043,0xc0000022,0x00000001,0x40000002,0xc0000043,0x40000062,0x80000001,0x40000042,0x40000042,0x40000002,0x00000002,0x00000040,0x80000002,0x80000000,0x80000002,0x80000040,0x00000000,0x80000040,0x80000000,0x00000040,0x80000000,0x00000040,0x80000002,0x00000000,0x80000000,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000004,0x00000080,0x00000004,0x00000009,0x00000101,0x00000009,0x00000012,0x00000202,0x0000001a,0x00000124,0x0000040c,0x00000026,0x0000004a,0x0000080a } } +, {1,52,0,65,0,15, { 0x04000010,0xe8000000,0x0800000c,0x18000000,0xb800000a,0xc8000010,0x2c000010,0xf4000014,0xb4000008,0x08000000,0x9800000c,0xd8000010,0x08000010,0xb8000010,0x98000000,0x60000000,0x00000008,0xc0000000,0x90000014,0x10000010,0xb8000014,0x28000000,0x20000010,0x48000000,0x08000018,0x60000000,0x90000010,0xf0000010,0x90000008,0xc0000000,0x90000010,0xf0000010,0xb0000008,0x40000000,0x90000000,0xf0000010,0x90000018,0x60000000,0x90000010,0x90000010,0x90000000,0x80000000,0x00000010,0xa0000000,0x20000000,0xa0000000,0x20000010,0x00000000,0x20000010,0x20000000,0x00000010,0x20000000,0x00000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000040,0x40000002,0x80000004,0x80000080,0x80000006,0x00000049,0x00000103,0x80000009,0x80000012 } } +, {2,45,0,58,0,16, { 0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046,0x4000004b,0x80000107,0x00000089,0x00000014,0x8000024b,0x0000011b,0x8000016d,0x8000041a,0x000002e4,0x80000054,0x00000967 } } +, {2,46,0,58,0,17, { 0x2400001c,0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046,0x4000004b,0x80000107,0x00000089,0x00000014,0x8000024b,0x0000011b,0x8000016d,0x8000041a,0x000002e4,0x80000054 } } +, {2,46,2,58,0,18, { 0x90000070,0xb0000053,0x30000008,0x00000043,0xd0000072,0xb0000010,0xf0000062,0xc0000042,0x00000030,0xe0000042,0x20000060,0xe0000041,0x20000050,0xc0000041,0xe0000072,0xa0000003,0xc0000012,0x60000041,0xc0000032,0x20000001,0xc0000002,0xe0000042,0x60000042,0x80000002,0x00000000,0x00000000,0x80000000,0x00000002,0x00000040,0x00000000,0x80000040,0x80000000,0x00000040,0x80000001,0x00000060,0x80000003,0x40000002,0xc0000040,0xc0000002,0x80000000,0x80000000,0x80000002,0x00000040,0x00000002,0x80000000,0x80000000,0x80000000,0x00000002,0x00000040,0x00000000,0x80000040,0x80000002,0x00000000,0x80000000,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000004,0x00000080,0x00000004,0x00000009,0x00000105,0x00000089,0x00000016,0x0000020b,0x0000011b,0x0000012d,0x0000041e,0x00000224,0x00000050,0x0000092e,0x0000046c,0x000005b6,0x0000106a,0x00000b90,0x00000152 } } +, {2,47,0,58,0,19, { 0x20000010,0x2400001c,0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046,0x4000004b,0x80000107,0x00000089,0x00000014,0x8000024b,0x0000011b,0x8000016d,0x8000041a,0x000002e4 } } +, {2,48,0,58,0,20, { 0xbc00001a,0x20000010,0x2400001c,0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046,0x4000004b,0x80000107,0x00000089,0x00000014,0x8000024b,0x0000011b,0x8000016d,0x8000041a } } +, {2,49,0,58,0,21, { 0x3c000004,0xbc00001a,0x20000010,0x2400001c,0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046,0x4000004b,0x80000107,0x00000089,0x00000014,0x8000024b,0x0000011b,0x8000016d } } +, {2,49,2,58,0,22, { 0xf0000010,0xf000006a,0x80000040,0x90000070,0xb0000053,0x30000008,0x00000043,0xd0000072,0xb0000010,0xf0000062,0xc0000042,0x00000030,0xe0000042,0x20000060,0xe0000041,0x20000050,0xc0000041,0xe0000072,0xa0000003,0xc0000012,0x60000041,0xc0000032,0x20000001,0xc0000002,0xe0000042,0x60000042,0x80000002,0x00000000,0x00000000,0x80000000,0x00000002,0x00000040,0x00000000,0x80000040,0x80000000,0x00000040,0x80000001,0x00000060,0x80000003,0x40000002,0xc0000040,0xc0000002,0x80000000,0x80000000,0x80000002,0x00000040,0x00000002,0x80000000,0x80000000,0x80000000,0x00000002,0x00000040,0x00000000,0x80000040,0x80000002,0x00000000,0x80000000,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000004,0x00000080,0x00000004,0x00000009,0x00000105,0x00000089,0x00000016,0x0000020b,0x0000011b,0x0000012d,0x0000041e,0x00000224,0x00000050,0x0000092e,0x0000046c,0x000005b6 } } +, {2,50,0,65,0,23, { 0xb400001c,0x3c000004,0xbc00001a,0x20000010,0x2400001c,0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046,0x4000004b,0x80000107,0x00000089,0x00000014,0x8000024b,0x0000011b } } +, {2,50,2,65,0,24, { 0xd0000072,0xf0000010,0xf000006a,0x80000040,0x90000070,0xb0000053,0x30000008,0x00000043,0xd0000072,0xb0000010,0xf0000062,0xc0000042,0x00000030,0xe0000042,0x20000060,0xe0000041,0x20000050,0xc0000041,0xe0000072,0xa0000003,0xc0000012,0x60000041,0xc0000032,0x20000001,0xc0000002,0xe0000042,0x60000042,0x80000002,0x00000000,0x00000000,0x80000000,0x00000002,0x00000040,0x00000000,0x80000040,0x80000000,0x00000040,0x80000001,0x00000060,0x80000003,0x40000002,0xc0000040,0xc0000002,0x80000000,0x80000000,0x80000002,0x00000040,0x00000002,0x80000000,0x80000000,0x80000000,0x00000002,0x00000040,0x00000000,0x80000040,0x80000002,0x00000000,0x80000000,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000004,0x00000080,0x00000004,0x00000009,0x00000105,0x00000089,0x00000016,0x0000020b,0x0000011b,0x0000012d,0x0000041e,0x00000224,0x00000050,0x0000092e,0x0000046c } } +, {2,51,0,65,0,25, { 0xc0000010,0xb400001c,0x3c000004,0xbc00001a,0x20000010,0x2400001c,0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046,0x4000004b,0x80000107,0x00000089,0x00000014,0x8000024b } } +, {2,51,2,65,0,26, { 0x00000043,0xd0000072,0xf0000010,0xf000006a,0x80000040,0x90000070,0xb0000053,0x30000008,0x00000043,0xd0000072,0xb0000010,0xf0000062,0xc0000042,0x00000030,0xe0000042,0x20000060,0xe0000041,0x20000050,0xc0000041,0xe0000072,0xa0000003,0xc0000012,0x60000041,0xc0000032,0x20000001,0xc0000002,0xe0000042,0x60000042,0x80000002,0x00000000,0x00000000,0x80000000,0x00000002,0x00000040,0x00000000,0x80000040,0x80000000,0x00000040,0x80000001,0x00000060,0x80000003,0x40000002,0xc0000040,0xc0000002,0x80000000,0x80000000,0x80000002,0x00000040,0x00000002,0x80000000,0x80000000,0x80000000,0x00000002,0x00000040,0x00000000,0x80000040,0x80000002,0x00000000,0x80000000,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000004,0x00000080,0x00000004,0x00000009,0x00000105,0x00000089,0x00000016,0x0000020b,0x0000011b,0x0000012d,0x0000041e,0x00000224,0x00000050,0x0000092e } } +, {2,52,0,65,0,27, { 0x0c000002,0xc0000010,0xb400001c,0x3c000004,0xbc00001a,0x20000010,0x2400001c,0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046,0x4000004b,0x80000107,0x00000089,0x00000014 } } +, {2,53,0,65,0,28, { 0xcc000014,0x0c000002,0xc0000010,0xb400001c,0x3c000004,0xbc00001a,0x20000010,0x2400001c,0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046,0x4000004b,0x80000107,0x00000089 } } +, {2,54,0,65,0,29, { 0x0400001c,0xcc000014,0x0c000002,0xc0000010,0xb400001c,0x3c000004,0xbc00001a,0x20000010,0x2400001c,0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046,0x4000004b,0x80000107 } } +, {2,55,0,65,0,30, { 0x00000010,0x0400001c,0xcc000014,0x0c000002,0xc0000010,0xb400001c,0x3c000004,0xbc00001a,0x20000010,0x2400001c,0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046,0x4000004b } } +, {2,56,0,65,0,31, { 0x2600001a,0x00000010,0x0400001c,0xcc000014,0x0c000002,0xc0000010,0xb400001c,0x3c000004,0xbc00001a,0x20000010,0x2400001c,0xec000014,0x0c000002,0xc0000010,0xb400001c,0x2c000004,0xbc000018,0xb0000010,0x0000000c,0xb8000010,0x08000018,0x78000010,0x08000014,0x70000010,0xb800001c,0xe8000000,0xb0000004,0x58000010,0xb000000c,0x48000000,0xb0000000,0xb8000010,0x98000010,0xa0000000,0x00000000,0x00000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0x20000000,0x00000010,0x60000000,0x00000018,0xe0000000,0x90000000,0x30000010,0xb0000000,0x20000000,0x20000000,0xa0000000,0x00000010,0x80000000,0x20000000,0x20000000,0x20000000,0x80000000,0x00000010,0x00000000,0x20000010,0xa0000000,0x00000000,0x20000000,0x20000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000001,0x00000020,0x00000001,0x40000002,0x40000041,0x40000022,0x80000005,0xc0000082,0xc0000046 } } +, {0,0,0,0,0,0, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}} +}; +void ubc_check(const uint32_t W[80], uint32_t dvmask[1]) +{ + uint32_t mask = ~((uint32_t)(0)); + mask &= (((((W[44]^W[45])>>29)&1)-1) | ~(DV_I_48_0_bit|DV_I_51_0_bit|DV_I_52_0_bit|DV_II_45_0_bit|DV_II_46_0_bit|DV_II_50_0_bit|DV_II_51_0_bit)); + mask &= (((((W[49]^W[50])>>29)&1)-1) | ~(DV_I_46_0_bit|DV_II_45_0_bit|DV_II_50_0_bit|DV_II_51_0_bit|DV_II_55_0_bit|DV_II_56_0_bit)); + mask &= (((((W[48]^W[49])>>29)&1)-1) | ~(DV_I_45_0_bit|DV_I_52_0_bit|DV_II_49_0_bit|DV_II_50_0_bit|DV_II_54_0_bit|DV_II_55_0_bit)); + mask &= ((((W[47]^(W[50]>>25))&(1<<4))-(1<<4)) | ~(DV_I_47_0_bit|DV_I_49_0_bit|DV_I_51_0_bit|DV_II_45_0_bit|DV_II_51_0_bit|DV_II_56_0_bit)); + mask &= (((((W[47]^W[48])>>29)&1)-1) | ~(DV_I_44_0_bit|DV_I_51_0_bit|DV_II_48_0_bit|DV_II_49_0_bit|DV_II_53_0_bit|DV_II_54_0_bit)); + mask &= (((((W[46]>>4)^(W[49]>>29))&1)-1) | ~(DV_I_46_0_bit|DV_I_48_0_bit|DV_I_50_0_bit|DV_I_52_0_bit|DV_II_50_0_bit|DV_II_55_0_bit)); + mask &= (((((W[46]^W[47])>>29)&1)-1) | ~(DV_I_43_0_bit|DV_I_50_0_bit|DV_II_47_0_bit|DV_II_48_0_bit|DV_II_52_0_bit|DV_II_53_0_bit)); + mask &= (((((W[45]>>4)^(W[48]>>29))&1)-1) | ~(DV_I_45_0_bit|DV_I_47_0_bit|DV_I_49_0_bit|DV_I_51_0_bit|DV_II_49_0_bit|DV_II_54_0_bit)); + mask &= (((((W[45]^W[46])>>29)&1)-1) | ~(DV_I_49_0_bit|DV_I_52_0_bit|DV_II_46_0_bit|DV_II_47_0_bit|DV_II_51_0_bit|DV_II_52_0_bit)); + mask &= (((((W[44]>>4)^(W[47]>>29))&1)-1) | ~(DV_I_44_0_bit|DV_I_46_0_bit|DV_I_48_0_bit|DV_I_50_0_bit|DV_II_48_0_bit|DV_II_53_0_bit)); + mask &= (((((W[43]>>4)^(W[46]>>29))&1)-1) | ~(DV_I_43_0_bit|DV_I_45_0_bit|DV_I_47_0_bit|DV_I_49_0_bit|DV_II_47_0_bit|DV_II_52_0_bit)); + mask &= (((((W[43]^W[44])>>29)&1)-1) | ~(DV_I_47_0_bit|DV_I_50_0_bit|DV_I_51_0_bit|DV_II_45_0_bit|DV_II_49_0_bit|DV_II_50_0_bit)); + mask &= (((((W[42]>>4)^(W[45]>>29))&1)-1) | ~(DV_I_44_0_bit|DV_I_46_0_bit|DV_I_48_0_bit|DV_I_52_0_bit|DV_II_46_0_bit|DV_II_51_0_bit)); + mask &= (((((W[41]>>4)^(W[44]>>29))&1)-1) | ~(DV_I_43_0_bit|DV_I_45_0_bit|DV_I_47_0_bit|DV_I_51_0_bit|DV_II_45_0_bit|DV_II_50_0_bit)); + mask &= (((((W[40]^W[41])>>29)&1)-1) | ~(DV_I_44_0_bit|DV_I_47_0_bit|DV_I_48_0_bit|DV_II_46_0_bit|DV_II_47_0_bit|DV_II_56_0_bit)); + mask &= (((((W[54]^W[55])>>29)&1)-1) | ~(DV_I_51_0_bit|DV_II_47_0_bit|DV_II_50_0_bit|DV_II_55_0_bit|DV_II_56_0_bit)); + mask &= (((((W[53]^W[54])>>29)&1)-1) | ~(DV_I_50_0_bit|DV_II_46_0_bit|DV_II_49_0_bit|DV_II_54_0_bit|DV_II_55_0_bit)); + mask &= (((((W[52]^W[53])>>29)&1)-1) | ~(DV_I_49_0_bit|DV_II_45_0_bit|DV_II_48_0_bit|DV_II_53_0_bit|DV_II_54_0_bit)); + mask &= ((((W[50]^(W[53]>>25))&(1<<4))-(1<<4)) | ~(DV_I_50_0_bit|DV_I_52_0_bit|DV_II_46_0_bit|DV_II_48_0_bit|DV_II_54_0_bit)); + mask &= (((((W[50]^W[51])>>29)&1)-1) | ~(DV_I_47_0_bit|DV_II_46_0_bit|DV_II_51_0_bit|DV_II_52_0_bit|DV_II_56_0_bit)); + mask &= ((((W[49]^(W[52]>>25))&(1<<4))-(1<<4)) | ~(DV_I_49_0_bit|DV_I_51_0_bit|DV_II_45_0_bit|DV_II_47_0_bit|DV_II_53_0_bit)); + mask &= ((((W[48]^(W[51]>>25))&(1<<4))-(1<<4)) | ~(DV_I_48_0_bit|DV_I_50_0_bit|DV_I_52_0_bit|DV_II_46_0_bit|DV_II_52_0_bit)); + mask &= (((((W[42]^W[43])>>29)&1)-1) | ~(DV_I_46_0_bit|DV_I_49_0_bit|DV_I_50_0_bit|DV_II_48_0_bit|DV_II_49_0_bit)); + mask &= (((((W[41]^W[42])>>29)&1)-1) | ~(DV_I_45_0_bit|DV_I_48_0_bit|DV_I_49_0_bit|DV_II_47_0_bit|DV_II_48_0_bit)); + mask &= (((((W[40]>>4)^(W[43]>>29))&1)-1) | ~(DV_I_44_0_bit|DV_I_46_0_bit|DV_I_50_0_bit|DV_II_49_0_bit|DV_II_56_0_bit)); + mask &= (((((W[39]>>4)^(W[42]>>29))&1)-1) | ~(DV_I_43_0_bit|DV_I_45_0_bit|DV_I_49_0_bit|DV_II_48_0_bit|DV_II_55_0_bit)); + if (mask & (DV_I_44_0_bit|DV_I_48_0_bit|DV_II_47_0_bit|DV_II_54_0_bit|DV_II_56_0_bit)) + mask &= (((((W[38]>>4)^(W[41]>>29))&1)-1) | ~(DV_I_44_0_bit|DV_I_48_0_bit|DV_II_47_0_bit|DV_II_54_0_bit|DV_II_56_0_bit)); + mask &= (((((W[37]>>4)^(W[40]>>29))&1)-1) | ~(DV_I_43_0_bit|DV_I_47_0_bit|DV_II_46_0_bit|DV_II_53_0_bit|DV_II_55_0_bit)); + if (mask & (DV_I_52_0_bit|DV_II_48_0_bit|DV_II_51_0_bit|DV_II_56_0_bit)) + mask &= (((((W[55]^W[56])>>29)&1)-1) | ~(DV_I_52_0_bit|DV_II_48_0_bit|DV_II_51_0_bit|DV_II_56_0_bit)); + if (mask & (DV_I_52_0_bit|DV_II_48_0_bit|DV_II_50_0_bit|DV_II_56_0_bit)) + mask &= ((((W[52]^(W[55]>>25))&(1<<4))-(1<<4)) | ~(DV_I_52_0_bit|DV_II_48_0_bit|DV_II_50_0_bit|DV_II_56_0_bit)); + if (mask & (DV_I_51_0_bit|DV_II_47_0_bit|DV_II_49_0_bit|DV_II_55_0_bit)) + mask &= ((((W[51]^(W[54]>>25))&(1<<4))-(1<<4)) | ~(DV_I_51_0_bit|DV_II_47_0_bit|DV_II_49_0_bit|DV_II_55_0_bit)); + if (mask & (DV_I_48_0_bit|DV_II_47_0_bit|DV_II_52_0_bit|DV_II_53_0_bit)) + mask &= (((((W[51]^W[52])>>29)&1)-1) | ~(DV_I_48_0_bit|DV_II_47_0_bit|DV_II_52_0_bit|DV_II_53_0_bit)); + if (mask & (DV_I_46_0_bit|DV_I_49_0_bit|DV_II_45_0_bit|DV_II_48_0_bit)) + mask &= (((((W[36]>>4)^(W[40]>>29))&1)-1) | ~(DV_I_46_0_bit|DV_I_49_0_bit|DV_II_45_0_bit|DV_II_48_0_bit)); + if (mask & (DV_I_52_0_bit|DV_II_48_0_bit|DV_II_49_0_bit)) + mask &= ((0-(((W[53]^W[56])>>29)&1)) | ~(DV_I_52_0_bit|DV_II_48_0_bit|DV_II_49_0_bit)); + if (mask & (DV_I_50_0_bit|DV_II_46_0_bit|DV_II_47_0_bit)) + mask &= ((0-(((W[51]^W[54])>>29)&1)) | ~(DV_I_50_0_bit|DV_II_46_0_bit|DV_II_47_0_bit)); + if (mask & (DV_I_49_0_bit|DV_I_51_0_bit|DV_II_45_0_bit)) + mask &= ((0-(((W[50]^W[52])>>29)&1)) | ~(DV_I_49_0_bit|DV_I_51_0_bit|DV_II_45_0_bit)); + if (mask & (DV_I_48_0_bit|DV_I_50_0_bit|DV_I_52_0_bit)) + mask &= ((0-(((W[49]^W[51])>>29)&1)) | ~(DV_I_48_0_bit|DV_I_50_0_bit|DV_I_52_0_bit)); + if (mask & (DV_I_47_0_bit|DV_I_49_0_bit|DV_I_51_0_bit)) + mask &= ((0-(((W[48]^W[50])>>29)&1)) | ~(DV_I_47_0_bit|DV_I_49_0_bit|DV_I_51_0_bit)); + if (mask & (DV_I_46_0_bit|DV_I_48_0_bit|DV_I_50_0_bit)) + mask &= ((0-(((W[47]^W[49])>>29)&1)) | ~(DV_I_46_0_bit|DV_I_48_0_bit|DV_I_50_0_bit)); + if (mask & (DV_I_45_0_bit|DV_I_47_0_bit|DV_I_49_0_bit)) + mask &= ((0-(((W[46]^W[48])>>29)&1)) | ~(DV_I_45_0_bit|DV_I_47_0_bit|DV_I_49_0_bit)); + mask &= ((((W[45]^W[47])&(1<<6))-(1<<6)) | ~(DV_I_47_2_bit|DV_I_49_2_bit|DV_I_51_2_bit)); + if (mask & (DV_I_44_0_bit|DV_I_46_0_bit|DV_I_48_0_bit)) + mask &= ((0-(((W[45]^W[47])>>29)&1)) | ~(DV_I_44_0_bit|DV_I_46_0_bit|DV_I_48_0_bit)); + mask &= (((((W[44]^W[46])>>6)&1)-1) | ~(DV_I_46_2_bit|DV_I_48_2_bit|DV_I_50_2_bit)); + if (mask & (DV_I_43_0_bit|DV_I_45_0_bit|DV_I_47_0_bit)) + mask &= ((0-(((W[44]^W[46])>>29)&1)) | ~(DV_I_43_0_bit|DV_I_45_0_bit|DV_I_47_0_bit)); + mask &= ((0-((W[41]^(W[42]>>5))&(1<<1))) | ~(DV_I_48_2_bit|DV_II_46_2_bit|DV_II_51_2_bit)); + mask &= ((0-((W[40]^(W[41]>>5))&(1<<1))) | ~(DV_I_47_2_bit|DV_I_51_2_bit|DV_II_50_2_bit)); + if (mask & (DV_I_44_0_bit|DV_I_46_0_bit|DV_II_56_0_bit)) + mask &= ((0-(((W[40]^W[42])>>4)&1)) | ~(DV_I_44_0_bit|DV_I_46_0_bit|DV_II_56_0_bit)); + mask &= ((0-((W[39]^(W[40]>>5))&(1<<1))) | ~(DV_I_46_2_bit|DV_I_50_2_bit|DV_II_49_2_bit)); + if (mask & (DV_I_43_0_bit|DV_I_45_0_bit|DV_II_55_0_bit)) + mask &= ((0-(((W[39]^W[41])>>4)&1)) | ~(DV_I_43_0_bit|DV_I_45_0_bit|DV_II_55_0_bit)); + if (mask & (DV_I_44_0_bit|DV_II_54_0_bit|DV_II_56_0_bit)) + mask &= ((0-(((W[38]^W[40])>>4)&1)) | ~(DV_I_44_0_bit|DV_II_54_0_bit|DV_II_56_0_bit)); + if (mask & (DV_I_43_0_bit|DV_II_53_0_bit|DV_II_55_0_bit)) + mask &= ((0-(((W[37]^W[39])>>4)&1)) | ~(DV_I_43_0_bit|DV_II_53_0_bit|DV_II_55_0_bit)); + mask &= ((0-((W[36]^(W[37]>>5))&(1<<1))) | ~(DV_I_47_2_bit|DV_I_50_2_bit|DV_II_46_2_bit)); + if (mask & (DV_I_45_0_bit|DV_I_48_0_bit|DV_II_47_0_bit)) + mask &= (((((W[35]>>4)^(W[39]>>29))&1)-1) | ~(DV_I_45_0_bit|DV_I_48_0_bit|DV_II_47_0_bit)); + if (mask & (DV_I_48_0_bit|DV_II_48_0_bit)) + mask &= ((0-((W[63]^(W[64]>>5))&(1<<0))) | ~(DV_I_48_0_bit|DV_II_48_0_bit)); + if (mask & (DV_I_45_0_bit|DV_II_45_0_bit)) + mask &= ((0-((W[63]^(W[64]>>5))&(1<<1))) | ~(DV_I_45_0_bit|DV_II_45_0_bit)); + if (mask & (DV_I_47_0_bit|DV_II_47_0_bit)) + mask &= ((0-((W[62]^(W[63]>>5))&(1<<0))) | ~(DV_I_47_0_bit|DV_II_47_0_bit)); + if (mask & (DV_I_46_0_bit|DV_II_46_0_bit)) + mask &= ((0-((W[61]^(W[62]>>5))&(1<<0))) | ~(DV_I_46_0_bit|DV_II_46_0_bit)); + mask &= ((0-((W[61]^(W[62]>>5))&(1<<2))) | ~(DV_I_46_2_bit|DV_II_46_2_bit)); + if (mask & (DV_I_45_0_bit|DV_II_45_0_bit)) + mask &= ((0-((W[60]^(W[61]>>5))&(1<<0))) | ~(DV_I_45_0_bit|DV_II_45_0_bit)); + if (mask & (DV_II_51_0_bit|DV_II_54_0_bit)) + mask &= (((((W[58]^W[59])>>29)&1)-1) | ~(DV_II_51_0_bit|DV_II_54_0_bit)); + if (mask & (DV_II_50_0_bit|DV_II_53_0_bit)) + mask &= (((((W[57]^W[58])>>29)&1)-1) | ~(DV_II_50_0_bit|DV_II_53_0_bit)); + if (mask & (DV_II_52_0_bit|DV_II_54_0_bit)) + mask &= ((((W[56]^(W[59]>>25))&(1<<4))-(1<<4)) | ~(DV_II_52_0_bit|DV_II_54_0_bit)); + if (mask & (DV_II_51_0_bit|DV_II_52_0_bit)) + mask &= ((0-(((W[56]^W[59])>>29)&1)) | ~(DV_II_51_0_bit|DV_II_52_0_bit)); + if (mask & (DV_II_49_0_bit|DV_II_52_0_bit)) + mask &= (((((W[56]^W[57])>>29)&1)-1) | ~(DV_II_49_0_bit|DV_II_52_0_bit)); + if (mask & (DV_II_51_0_bit|DV_II_53_0_bit)) + mask &= ((((W[55]^(W[58]>>25))&(1<<4))-(1<<4)) | ~(DV_II_51_0_bit|DV_II_53_0_bit)); + if (mask & (DV_II_50_0_bit|DV_II_52_0_bit)) + mask &= ((((W[54]^(W[57]>>25))&(1<<4))-(1<<4)) | ~(DV_II_50_0_bit|DV_II_52_0_bit)); + if (mask & (DV_II_49_0_bit|DV_II_51_0_bit)) + mask &= ((((W[53]^(W[56]>>25))&(1<<4))-(1<<4)) | ~(DV_II_49_0_bit|DV_II_51_0_bit)); + mask &= ((((W[51]^(W[50]>>5))&(1<<1))-(1<<1)) | ~(DV_I_50_2_bit|DV_II_46_2_bit)); + mask &= ((((W[48]^W[50])&(1<<6))-(1<<6)) | ~(DV_I_50_2_bit|DV_II_46_2_bit)); + if (mask & (DV_I_51_0_bit|DV_I_52_0_bit)) + mask &= ((0-(((W[48]^W[55])>>29)&1)) | ~(DV_I_51_0_bit|DV_I_52_0_bit)); + mask &= ((((W[47]^W[49])&(1<<6))-(1<<6)) | ~(DV_I_49_2_bit|DV_I_51_2_bit)); + mask &= ((((W[48]^(W[47]>>5))&(1<<1))-(1<<1)) | ~(DV_I_47_2_bit|DV_II_51_2_bit)); + mask &= ((((W[46]^W[48])&(1<<6))-(1<<6)) | ~(DV_I_48_2_bit|DV_I_50_2_bit)); + mask &= ((((W[47]^(W[46]>>5))&(1<<1))-(1<<1)) | ~(DV_I_46_2_bit|DV_II_50_2_bit)); + mask &= ((0-((W[44]^(W[45]>>5))&(1<<1))) | ~(DV_I_51_2_bit|DV_II_49_2_bit)); + mask &= ((((W[43]^W[45])&(1<<6))-(1<<6)) | ~(DV_I_47_2_bit|DV_I_49_2_bit)); + mask &= (((((W[42]^W[44])>>6)&1)-1) | ~(DV_I_46_2_bit|DV_I_48_2_bit)); + mask &= ((((W[43]^(W[42]>>5))&(1<<1))-(1<<1)) | ~(DV_II_46_2_bit|DV_II_51_2_bit)); + mask &= ((((W[42]^(W[41]>>5))&(1<<1))-(1<<1)) | ~(DV_I_51_2_bit|DV_II_50_2_bit)); + mask &= ((((W[41]^(W[40]>>5))&(1<<1))-(1<<1)) | ~(DV_I_50_2_bit|DV_II_49_2_bit)); + if (mask & (DV_I_52_0_bit|DV_II_51_0_bit)) + mask &= ((((W[39]^(W[43]>>25))&(1<<4))-(1<<4)) | ~(DV_I_52_0_bit|DV_II_51_0_bit)); + if (mask & (DV_I_51_0_bit|DV_II_50_0_bit)) + mask &= ((((W[38]^(W[42]>>25))&(1<<4))-(1<<4)) | ~(DV_I_51_0_bit|DV_II_50_0_bit)); + if (mask & (DV_I_48_2_bit|DV_I_51_2_bit)) + mask &= ((0-((W[37]^(W[38]>>5))&(1<<1))) | ~(DV_I_48_2_bit|DV_I_51_2_bit)); + if (mask & (DV_I_50_0_bit|DV_II_49_0_bit)) + mask &= ((((W[37]^(W[41]>>25))&(1<<4))-(1<<4)) | ~(DV_I_50_0_bit|DV_II_49_0_bit)); + if (mask & (DV_II_52_0_bit|DV_II_54_0_bit)) + mask &= ((0-((W[36]^W[38])&(1<<4))) | ~(DV_II_52_0_bit|DV_II_54_0_bit)); + mask &= ((0-((W[35]^(W[36]>>5))&(1<<1))) | ~(DV_I_46_2_bit|DV_I_49_2_bit)); + if (mask & (DV_I_51_0_bit|DV_II_47_0_bit)) + mask &= ((((W[35]^(W[39]>>25))&(1<<3))-(1<<3)) | ~(DV_I_51_0_bit|DV_II_47_0_bit)); +if (mask) { + + if (mask & DV_I_43_0_bit) + if ( + !((W[61]^(W[62]>>5)) & (1<<1)) + || !(!((W[59]^(W[63]>>25)) & (1<<5))) + || !((W[58]^(W[63]>>30)) & (1<<0)) + ) mask &= ~DV_I_43_0_bit; + if (mask & DV_I_44_0_bit) + if ( + !((W[62]^(W[63]>>5)) & (1<<1)) + || !(!((W[60]^(W[64]>>25)) & (1<<5))) + || !((W[59]^(W[64]>>30)) & (1<<0)) + ) mask &= ~DV_I_44_0_bit; + if (mask & DV_I_46_2_bit) + mask &= ((~((W[40]^W[42])>>2)) | ~DV_I_46_2_bit); + if (mask & DV_I_47_2_bit) + if ( + !((W[62]^(W[63]>>5)) & (1<<2)) + || !(!((W[41]^W[43]) & (1<<6))) + ) mask &= ~DV_I_47_2_bit; + if (mask & DV_I_48_2_bit) + if ( + !((W[63]^(W[64]>>5)) & (1<<2)) + || !(!((W[48]^(W[49]<<5)) & (1<<6))) + ) mask &= ~DV_I_48_2_bit; + if (mask & DV_I_49_2_bit) + if ( + !(!((W[49]^(W[50]<<5)) & (1<<6))) + || !((W[42]^W[50]) & (1<<1)) + || !(!((W[39]^(W[40]<<5)) & (1<<6))) + || !((W[38]^W[40]) & (1<<1)) + ) mask &= ~DV_I_49_2_bit; + if (mask & DV_I_50_0_bit) + mask &= ((((W[36]^W[37])<<7)) | ~DV_I_50_0_bit); + if (mask & DV_I_50_2_bit) + mask &= ((((W[43]^W[51])<<11)) | ~DV_I_50_2_bit); + if (mask & DV_I_51_0_bit) + mask &= ((((W[37]^W[38])<<9)) | ~DV_I_51_0_bit); + if (mask & DV_I_51_2_bit) + if ( + !(!((W[51]^(W[52]<<5)) & (1<<6))) + || !(!((W[49]^W[51]) & (1<<6))) + || !(!((W[37]^(W[37]>>5)) & (1<<1))) + || !(!((W[35]^(W[39]>>25)) & (1<<5))) + ) mask &= ~DV_I_51_2_bit; + if (mask & DV_I_52_0_bit) + mask &= ((((W[38]^W[39])<<11)) | ~DV_I_52_0_bit); + if (mask & DV_II_46_2_bit) + mask &= ((((W[47]^W[51])<<17)) | ~DV_II_46_2_bit); + if (mask & DV_II_48_0_bit) + if ( + !(!((W[36]^(W[40]>>25)) & (1<<3))) + || !((W[35]^(W[40]<<2)) & (1<<30)) + ) mask &= ~DV_II_48_0_bit; + if (mask & DV_II_49_0_bit) + if ( + !(!((W[37]^(W[41]>>25)) & (1<<3))) + || !((W[36]^(W[41]<<2)) & (1<<30)) + ) mask &= ~DV_II_49_0_bit; + if (mask & DV_II_49_2_bit) + if ( + !(!((W[53]^(W[54]<<5)) & (1<<6))) + || !(!((W[51]^W[53]) & (1<<6))) + || !((W[50]^W[54]) & (1<<1)) + || !(!((W[45]^(W[46]<<5)) & (1<<6))) + || !(!((W[37]^(W[41]>>25)) & (1<<5))) + || !((W[36]^(W[41]>>30)) & (1<<0)) + ) mask &= ~DV_II_49_2_bit; + if (mask & DV_II_50_0_bit) + if ( + !((W[55]^W[58]) & (1<<29)) + || !(!((W[38]^(W[42]>>25)) & (1<<3))) + || !((W[37]^(W[42]<<2)) & (1<<30)) + ) mask &= ~DV_II_50_0_bit; + if (mask & DV_II_50_2_bit) + if ( + !(!((W[54]^(W[55]<<5)) & (1<<6))) + || !(!((W[52]^W[54]) & (1<<6))) + || !((W[51]^W[55]) & (1<<1)) + || !((W[45]^W[47]) & (1<<1)) + || !(!((W[38]^(W[42]>>25)) & (1<<5))) + || !((W[37]^(W[42]>>30)) & (1<<0)) + ) mask &= ~DV_II_50_2_bit; + if (mask & DV_II_51_0_bit) + if ( + !(!((W[39]^(W[43]>>25)) & (1<<3))) + || !((W[38]^(W[43]<<2)) & (1<<30)) + ) mask &= ~DV_II_51_0_bit; + if (mask & DV_II_51_2_bit) + if ( + !(!((W[55]^(W[56]<<5)) & (1<<6))) + || !(!((W[53]^W[55]) & (1<<6))) + || !((W[52]^W[56]) & (1<<1)) + || !((W[46]^W[48]) & (1<<1)) + || !(!((W[39]^(W[43]>>25)) & (1<<5))) + || !((W[38]^(W[43]>>30)) & (1<<0)) + ) mask &= ~DV_II_51_2_bit; + if (mask & DV_II_52_0_bit) + if ( + !(!((W[59]^W[60]) & (1<<29))) + || !(!((W[40]^(W[44]>>25)) & (1<<3))) + || !(!((W[40]^(W[44]>>25)) & (1<<4))) + || !((W[39]^(W[44]<<2)) & (1<<30)) + ) mask &= ~DV_II_52_0_bit; + if (mask & DV_II_53_0_bit) + if ( + !((W[58]^W[61]) & (1<<29)) + || !(!((W[57]^(W[61]>>25)) & (1<<4))) + || !(!((W[41]^(W[45]>>25)) & (1<<3))) + || !(!((W[41]^(W[45]>>25)) & (1<<4))) + ) mask &= ~DV_II_53_0_bit; + if (mask & DV_II_54_0_bit) + if ( + !(!((W[58]^(W[62]>>25)) & (1<<4))) + || !(!((W[42]^(W[46]>>25)) & (1<<3))) + || !(!((W[42]^(W[46]>>25)) & (1<<4))) + ) mask &= ~DV_II_54_0_bit; + if (mask & DV_II_55_0_bit) + if ( + !(!((W[59]^(W[63]>>25)) & (1<<4))) + || !(!((W[57]^(W[59]>>25)) & (1<<4))) + || !(!((W[43]^(W[47]>>25)) & (1<<3))) + || !(!((W[43]^(W[47]>>25)) & (1<<4))) + ) mask &= ~DV_II_55_0_bit; + if (mask & DV_II_56_0_bit) + if ( + !(!((W[60]^(W[64]>>25)) & (1<<4))) + || !(!((W[44]^(W[48]>>25)) & (1<<3))) + || !(!((W[44]^(W[48]>>25)) & (1<<4))) + ) mask &= ~DV_II_56_0_bit; +} + + dvmask[0]=mask; +} + +#ifdef SHA1DC_CUSTOM_TRAILING_INCLUDE_UBC_CHECK_C +#include SHA1DC_CUSTOM_TRAILING_INCLUDE_UBC_CHECK_C +#endif diff --git a/src/util/hash/sha1dc/ubc_check.h b/src/util/hash/sha1dc/ubc_check.h new file mode 100644 index 0000000..d7e17dc --- /dev/null +++ b/src/util/hash/sha1dc/ubc_check.h @@ -0,0 +1,52 @@ +/*** +* Copyright 2017 Marc Stevens , Dan Shumow +* Distributed under the MIT Software License. +* See accompanying file LICENSE.txt or copy at +* https://opensource.org/licenses/MIT +***/ + +/* +// this file was generated by the 'parse_bitrel' program in the tools section +// using the data files from directory 'tools/data/3565' +// +// sha1_dvs contains a list of SHA-1 Disturbance Vectors (DV) to check +// dvType, dvK and dvB define the DV: I(K,B) or II(K,B) (see the paper) +// dm[80] is the expanded message block XOR-difference defined by the DV +// testt is the step to do the recompression from for collision detection +// maski and maskb define the bit to check for each DV in the dvmask returned by ubc_check +// +// ubc_check takes as input an expanded message block and verifies the unavoidable bitconditions for all listed DVs +// it returns a dvmask where each bit belonging to a DV is set if all unavoidable bitconditions for that DV have been met +// thus one needs to do the recompression check for each DV that has its bit set +*/ + +#ifndef SHA1DC_UBC_CHECK_H +#define SHA1DC_UBC_CHECK_H + +#if defined(__cplusplus) +extern "C" { +#endif + +#ifndef SHA1DC_NO_STANDARD_INCLUDES +#include +#endif + +#define DVMASKSIZE 1 +typedef struct { int dvType; int dvK; int dvB; int testt; int maski; int maskb; uint32_t dm[80]; } dv_info_t; +extern dv_info_t sha1_dvs[]; +void ubc_check(const uint32_t W[80], uint32_t dvmask[DVMASKSIZE]); + +#define DOSTORESTATE58 +#define DOSTORESTATE65 + +#define CHECK_DVMASK(_DVMASK) (0 != _DVMASK[0]) + +#if defined(__cplusplus) +} +#endif + +#ifdef SHA1DC_CUSTOM_TRAILING_INCLUDE_UBC_CHECK_H +#include SHA1DC_CUSTOM_TRAILING_INCLUDE_UBC_CHECK_H +#endif + +#endif diff --git a/src/util/hash/win32.c b/src/util/hash/win32.c new file mode 100644 index 0000000..f80c0d5 --- /dev/null +++ b/src/util/hash/win32.c @@ -0,0 +1,549 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "win32.h" + +#include "runtime.h" + +#include +#include + +#define GIT_HASH_CNG_DLL_NAME "bcrypt.dll" + +/* BCRYPT_SHA1_ALGORITHM */ +#define GIT_HASH_CNG_SHA1_TYPE L"SHA1" +#define GIT_HASH_CNG_SHA256_TYPE L"SHA256" + +/* BCRYPT_OBJECT_LENGTH */ +#define GIT_HASH_CNG_HASH_OBJECT_LEN L"ObjectLength" + +/* BCRYPT_HASH_REUSEABLE_FLAGS */ +#define GIT_HASH_CNG_HASH_REUSABLE 0x00000020 + +/* Definitions */ + +/* CryptoAPI is available for hashing on Windows XP and newer. */ +struct cryptoapi_provider { + HCRYPTPROV handle; +}; + +/* + * CNG (bcrypt.dll) is significantly more performant than CryptoAPI and is + * preferred, however it is only available on Windows 2008 and newer and + * must therefore be dynamically loaded, and we must inline constants that + * would not exist when building in pre-Windows 2008 environments. + */ + +/* Function declarations for CNG */ +typedef NTSTATUS (WINAPI *cng_open_algorithm_provider_fn)( + HANDLE /* BCRYPT_ALG_HANDLE */ *phAlgorithm, + LPCWSTR pszAlgId, + LPCWSTR pszImplementation, + DWORD dwFlags); + +typedef NTSTATUS (WINAPI *cng_get_property_fn)( + HANDLE /* BCRYPT_HANDLE */ hObject, + LPCWSTR pszProperty, + PUCHAR pbOutput, + ULONG cbOutput, + ULONG *pcbResult, + ULONG dwFlags); + +typedef NTSTATUS (WINAPI *cng_create_hash_fn)( + HANDLE /* BCRYPT_ALG_HANDLE */ hAlgorithm, + HANDLE /* BCRYPT_HASH_HANDLE */ *phHash, + PUCHAR pbHashObject, ULONG cbHashObject, + PUCHAR pbSecret, + ULONG cbSecret, + ULONG dwFlags); + +typedef NTSTATUS (WINAPI *cng_finish_hash_fn)( + HANDLE /* BCRYPT_HASH_HANDLE */ hHash, + PUCHAR pbOutput, + ULONG cbOutput, + ULONG dwFlags); + +typedef NTSTATUS (WINAPI *cng_hash_data_fn)( + HANDLE /* BCRYPT_HASH_HANDLE */ hHash, + PUCHAR pbInput, + ULONG cbInput, + ULONG dwFlags); + +typedef NTSTATUS (WINAPI *cng_destroy_hash_fn)( + HANDLE /* BCRYPT_HASH_HANDLE */ hHash); + +typedef NTSTATUS (WINAPI *cng_close_algorithm_provider_fn)( + HANDLE /* BCRYPT_ALG_HANDLE */ hAlgorithm, + ULONG dwFlags); + +struct cng_provider { + /* DLL for CNG */ + HINSTANCE dll; + + /* Function pointers for CNG */ + cng_open_algorithm_provider_fn open_algorithm_provider; + cng_get_property_fn get_property; + cng_create_hash_fn create_hash; + cng_finish_hash_fn finish_hash; + cng_hash_data_fn hash_data; + cng_destroy_hash_fn destroy_hash; + cng_close_algorithm_provider_fn close_algorithm_provider; + + HANDLE /* BCRYPT_ALG_HANDLE */ sha1_handle; + DWORD sha1_object_size; + + HANDLE /* BCRYPT_ALG_HANDLE */ sha256_handle; + DWORD sha256_object_size; +}; + +typedef struct { + git_hash_win32_provider_t type; + + union { + struct cryptoapi_provider cryptoapi; + struct cng_provider cng; + } provider; +} hash_win32_provider; + +/* Hash provider definition */ + +static hash_win32_provider hash_provider = {0}; + +/* Hash initialization */ + +/* Initialize CNG, if available */ +GIT_INLINE(int) cng_provider_init(void) +{ + char dll_path[MAX_PATH]; + DWORD dll_path_len, size_len; + + /* Only use CNG on Windows 2008 / Vista SP1 or better (Windows 6.0 SP1) */ + if (!git_has_win32_version(6, 0, 1)) { + git_error_set(GIT_ERROR_SHA, "CryptoNG is not supported on this platform"); + return -1; + } + + /* Load bcrypt.dll explicitly from the system directory */ + if ((dll_path_len = GetSystemDirectory(dll_path, MAX_PATH)) == 0 || + dll_path_len > MAX_PATH || + StringCchCat(dll_path, MAX_PATH, "\\") < 0 || + StringCchCat(dll_path, MAX_PATH, GIT_HASH_CNG_DLL_NAME) < 0 || + (hash_provider.provider.cng.dll = LoadLibrary(dll_path)) == NULL) { + git_error_set(GIT_ERROR_SHA, "CryptoNG library could not be loaded"); + return -1; + } + + /* Load the function addresses */ + if ((hash_provider.provider.cng.open_algorithm_provider = (cng_open_algorithm_provider_fn)((void *)GetProcAddress(hash_provider.provider.cng.dll, "BCryptOpenAlgorithmProvider"))) == NULL || + (hash_provider.provider.cng.get_property = (cng_get_property_fn)((void *)GetProcAddress(hash_provider.provider.cng.dll, "BCryptGetProperty"))) == NULL || + (hash_provider.provider.cng.create_hash = (cng_create_hash_fn)((void *)GetProcAddress(hash_provider.provider.cng.dll, "BCryptCreateHash"))) == NULL || + (hash_provider.provider.cng.finish_hash = (cng_finish_hash_fn)((void *)GetProcAddress(hash_provider.provider.cng.dll, "BCryptFinishHash"))) == NULL || + (hash_provider.provider.cng.hash_data = (cng_hash_data_fn)((void *)GetProcAddress(hash_provider.provider.cng.dll, "BCryptHashData"))) == NULL || + (hash_provider.provider.cng.destroy_hash = (cng_destroy_hash_fn)((void *)GetProcAddress(hash_provider.provider.cng.dll, "BCryptDestroyHash"))) == NULL || + (hash_provider.provider.cng.close_algorithm_provider = (cng_close_algorithm_provider_fn)((void *)GetProcAddress(hash_provider.provider.cng.dll, "BCryptCloseAlgorithmProvider"))) == NULL) { + FreeLibrary(hash_provider.provider.cng.dll); + + git_error_set(GIT_ERROR_OS, "CryptoNG functions could not be loaded"); + return -1; + } + + /* Load the SHA1 algorithm */ + if (hash_provider.provider.cng.open_algorithm_provider(&hash_provider.provider.cng.sha1_handle, GIT_HASH_CNG_SHA1_TYPE, NULL, GIT_HASH_CNG_HASH_REUSABLE) < 0 || + hash_provider.provider.cng.get_property(hash_provider.provider.cng.sha1_handle, GIT_HASH_CNG_HASH_OBJECT_LEN, (PBYTE)&hash_provider.provider.cng.sha1_object_size, sizeof(DWORD), &size_len, 0) < 0) { + git_error_set(GIT_ERROR_OS, "algorithm provider could not be initialized"); + goto on_error; + } + + /* Load the SHA256 algorithm */ + if (hash_provider.provider.cng.open_algorithm_provider(&hash_provider.provider.cng.sha256_handle, GIT_HASH_CNG_SHA256_TYPE, NULL, GIT_HASH_CNG_HASH_REUSABLE) < 0 || + hash_provider.provider.cng.get_property(hash_provider.provider.cng.sha256_handle, GIT_HASH_CNG_HASH_OBJECT_LEN, (PBYTE)&hash_provider.provider.cng.sha256_object_size, sizeof(DWORD), &size_len, 0) < 0) { + git_error_set(GIT_ERROR_OS, "algorithm provider could not be initialized"); + goto on_error; + } + + hash_provider.type = GIT_HASH_WIN32_CNG; + return 0; + +on_error: + if (hash_provider.provider.cng.sha1_handle) + hash_provider.provider.cng.close_algorithm_provider(hash_provider.provider.cng.sha1_handle, 0); + + if (hash_provider.provider.cng.sha256_handle) + hash_provider.provider.cng.close_algorithm_provider(hash_provider.provider.cng.sha256_handle, 0); + + if (hash_provider.provider.cng.dll) + FreeLibrary(hash_provider.provider.cng.dll); + + return -1; +} + +GIT_INLINE(void) cng_provider_shutdown(void) +{ + if (hash_provider.type == GIT_HASH_WIN32_INVALID) + return; + + hash_provider.provider.cng.close_algorithm_provider(hash_provider.provider.cng.sha1_handle, 0); + hash_provider.provider.cng.close_algorithm_provider(hash_provider.provider.cng.sha256_handle, 0); + FreeLibrary(hash_provider.provider.cng.dll); + + hash_provider.type = GIT_HASH_WIN32_INVALID; +} + +/* Initialize CryptoAPI */ +GIT_INLINE(int) cryptoapi_provider_init(void) +{ + if (!CryptAcquireContext(&hash_provider.provider.cryptoapi.handle, NULL, 0, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) { + git_error_set(GIT_ERROR_OS, "legacy hash context could not be started"); + return -1; + } + + hash_provider.type = GIT_HASH_WIN32_CRYPTOAPI; + return 0; +} + +GIT_INLINE(void) cryptoapi_provider_shutdown(void) +{ + if (hash_provider.type == GIT_HASH_WIN32_INVALID) + return; + + CryptReleaseContext(hash_provider.provider.cryptoapi.handle, 0); + + hash_provider.type = GIT_HASH_WIN32_INVALID; +} + +static void hash_provider_shutdown(void) +{ + if (hash_provider.type == GIT_HASH_WIN32_CNG) + cng_provider_shutdown(); + else if (hash_provider.type == GIT_HASH_WIN32_CRYPTOAPI) + cryptoapi_provider_shutdown(); +} + +static int hash_provider_init(void) +{ + int error = 0; + + if (hash_provider.type != GIT_HASH_WIN32_INVALID) + return 0; + + if ((error = cng_provider_init()) < 0) + error = cryptoapi_provider_init(); + + if (!error) + error = git_runtime_shutdown_register(hash_provider_shutdown); + + return error; +} + +git_hash_win32_provider_t git_hash_win32_provider(void) +{ + return hash_provider.type; +} + +int git_hash_win32_set_provider(git_hash_win32_provider_t provider) +{ + if (provider == hash_provider.type) + return 0; + + hash_provider_shutdown(); + + if (provider == GIT_HASH_WIN32_CNG) + return cng_provider_init(); + else if (provider == GIT_HASH_WIN32_CRYPTOAPI) + return cryptoapi_provider_init(); + + git_error_set(GIT_ERROR_SHA, "unsupported win32 provider"); + return -1; +} + +/* CryptoAPI: available in Windows XP and newer */ + +GIT_INLINE(int) hash_cryptoapi_init(git_hash_win32_ctx *ctx) +{ + if (ctx->ctx.cryptoapi.valid) + CryptDestroyHash(ctx->ctx.cryptoapi.hash_handle); + + if (!CryptCreateHash(hash_provider.provider.cryptoapi.handle, ctx->algorithm, 0, 0, &ctx->ctx.cryptoapi.hash_handle)) { + ctx->ctx.cryptoapi.valid = 0; + git_error_set(GIT_ERROR_OS, "legacy hash implementation could not be created"); + return -1; + } + + ctx->ctx.cryptoapi.valid = 1; + return 0; +} + +GIT_INLINE(int) hash_cryptoapi_update(git_hash_win32_ctx *ctx, const void *_data, size_t len) +{ + const BYTE *data = (BYTE *)_data; + + GIT_ASSERT(ctx->ctx.cryptoapi.valid); + + while (len > 0) { + DWORD chunk = (len > MAXDWORD) ? MAXDWORD : (DWORD)len; + + if (!CryptHashData(ctx->ctx.cryptoapi.hash_handle, data, chunk, 0)) { + git_error_set(GIT_ERROR_OS, "legacy hash data could not be updated"); + return -1; + } + + data += chunk; + len -= chunk; + } + + return 0; +} + +GIT_INLINE(int) hash_cryptoapi_final(unsigned char *out, git_hash_win32_ctx *ctx) +{ + DWORD len = ctx->algorithm == CALG_SHA_256 ? GIT_HASH_SHA256_SIZE : GIT_HASH_SHA1_SIZE; + int error = 0; + + GIT_ASSERT(ctx->ctx.cryptoapi.valid); + + if (!CryptGetHashParam(ctx->ctx.cryptoapi.hash_handle, HP_HASHVAL, out, &len, 0)) { + git_error_set(GIT_ERROR_OS, "legacy hash data could not be finished"); + error = -1; + } + + CryptDestroyHash(ctx->ctx.cryptoapi.hash_handle); + ctx->ctx.cryptoapi.valid = 0; + + return error; +} + +GIT_INLINE(void) hash_ctx_cryptoapi_cleanup(git_hash_win32_ctx *ctx) +{ + if (ctx->ctx.cryptoapi.valid) + CryptDestroyHash(ctx->ctx.cryptoapi.hash_handle); +} + +GIT_INLINE(int) hash_sha1_cryptoapi_ctx_init_init(git_hash_win32_ctx *ctx) +{ + ctx->algorithm = CALG_SHA1; + return hash_cryptoapi_init(ctx); +} + +GIT_INLINE(int) hash_sha256_cryptoapi_ctx_init(git_hash_win32_ctx *ctx) +{ + ctx->algorithm = CALG_SHA_256; + return hash_cryptoapi_init(ctx); +} + +/* CNG: Available in Windows Server 2008 and newer */ + +GIT_INLINE(int) hash_sha1_cng_ctx_init(git_hash_win32_ctx *ctx) +{ + if ((ctx->ctx.cng.hash_object = git__malloc(hash_provider.provider.cng.sha1_object_size)) == NULL) + return -1; + + if (hash_provider.provider.cng.create_hash(hash_provider.provider.cng.sha1_handle, &ctx->ctx.cng.hash_handle, ctx->ctx.cng.hash_object, hash_provider.provider.cng.sha1_object_size, NULL, 0, 0) < 0) { + git__free(ctx->ctx.cng.hash_object); + + git_error_set(GIT_ERROR_OS, "sha1 implementation could not be created"); + return -1; + } + + ctx->algorithm = CALG_SHA1; + return 0; +} + +GIT_INLINE(int) hash_sha256_cng_ctx_init(git_hash_win32_ctx *ctx) +{ + if ((ctx->ctx.cng.hash_object = git__malloc(hash_provider.provider.cng.sha256_object_size)) == NULL) + return -1; + + if (hash_provider.provider.cng.create_hash(hash_provider.provider.cng.sha256_handle, &ctx->ctx.cng.hash_handle, ctx->ctx.cng.hash_object, hash_provider.provider.cng.sha256_object_size, NULL, 0, 0) < 0) { + git__free(ctx->ctx.cng.hash_object); + + git_error_set(GIT_ERROR_OS, "sha256 implementation could not be created"); + return -1; + } + + ctx->algorithm = CALG_SHA_256; + return 0; +} + +GIT_INLINE(int) hash_cng_init(git_hash_win32_ctx *ctx) +{ + BYTE hash[GIT_HASH_SHA256_SIZE]; + ULONG size = ctx->algorithm == CALG_SHA_256 ? GIT_HASH_SHA256_SIZE : GIT_HASH_SHA1_SIZE; + + if (!ctx->ctx.cng.updated) + return 0; + + /* CNG needs to be finished to restart */ + if (hash_provider.provider.cng.finish_hash(ctx->ctx.cng.hash_handle, hash, size, 0) < 0) { + git_error_set(GIT_ERROR_OS, "hash implementation could not be finished"); + return -1; + } + + ctx->ctx.cng.updated = 0; + + return 0; +} + +GIT_INLINE(int) hash_cng_update(git_hash_win32_ctx *ctx, const void *_data, size_t len) +{ + PBYTE data = (PBYTE)_data; + + while (len > 0) { + ULONG chunk = (len > ULONG_MAX) ? ULONG_MAX : (ULONG)len; + + if (hash_provider.provider.cng.hash_data(ctx->ctx.cng.hash_handle, data, chunk, 0) < 0) { + git_error_set(GIT_ERROR_OS, "hash could not be updated"); + return -1; + } + + data += chunk; + len -= chunk; + } + + return 0; +} + +GIT_INLINE(int) hash_cng_final(unsigned char *out, git_hash_win32_ctx *ctx) +{ + ULONG size = ctx->algorithm == CALG_SHA_256 ? GIT_HASH_SHA256_SIZE : GIT_HASH_SHA1_SIZE; + + if (hash_provider.provider.cng.finish_hash(ctx->ctx.cng.hash_handle, out, size, 0) < 0) { + git_error_set(GIT_ERROR_OS, "hash could not be finished"); + return -1; + } + + ctx->ctx.cng.updated = 0; + + return 0; +} + +GIT_INLINE(void) hash_ctx_cng_cleanup(git_hash_win32_ctx *ctx) +{ + hash_provider.provider.cng.destroy_hash(ctx->ctx.cng.hash_handle); + git__free(ctx->ctx.cng.hash_object); +} + +/* Indirection between CryptoAPI and CNG */ + +GIT_INLINE(int) hash_sha1_win32_ctx_init(git_hash_win32_ctx *ctx) +{ + GIT_ASSERT_ARG(hash_provider.type); + + memset(ctx, 0x0, sizeof(git_hash_win32_ctx)); + return (hash_provider.type == GIT_HASH_WIN32_CNG) ? hash_sha1_cng_ctx_init(ctx) : hash_sha1_cryptoapi_ctx_init_init(ctx); +} + +GIT_INLINE(int) hash_sha256_win32_ctx_init(git_hash_win32_ctx *ctx) +{ + GIT_ASSERT_ARG(hash_provider.type); + + memset(ctx, 0x0, sizeof(git_hash_win32_ctx)); + return (hash_provider.type == GIT_HASH_WIN32_CNG) ? hash_sha256_cng_ctx_init(ctx) : hash_sha256_cryptoapi_ctx_init(ctx); +} + +GIT_INLINE(int) hash_win32_init(git_hash_win32_ctx *ctx) +{ + return (hash_provider.type == GIT_HASH_WIN32_CNG) ? hash_cng_init(ctx) : hash_cryptoapi_init(ctx); +} + +GIT_INLINE(int) hash_win32_update(git_hash_win32_ctx *ctx, const void *data, size_t len) +{ + return (hash_provider.type == GIT_HASH_WIN32_CNG) ? hash_cng_update(ctx, data, len) : hash_cryptoapi_update(ctx, data, len); +} + +GIT_INLINE(int) hash_win32_final(unsigned char *out, git_hash_win32_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + return (hash_provider.type == GIT_HASH_WIN32_CNG) ? hash_cng_final(out, ctx) : hash_cryptoapi_final(out, ctx); +} + +GIT_INLINE(void) hash_win32_cleanup(git_hash_win32_ctx *ctx) +{ + if (hash_provider.type == GIT_HASH_WIN32_CNG) + hash_ctx_cng_cleanup(ctx); + else if(hash_provider.type == GIT_HASH_WIN32_CRYPTOAPI) + hash_ctx_cryptoapi_cleanup(ctx); +} + +#ifdef GIT_SHA1_WIN32 + +int git_hash_sha1_global_init(void) +{ + return hash_provider_init(); +} + +int git_hash_sha1_ctx_init(git_hash_sha1_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + return hash_sha1_win32_ctx_init(&ctx->win32); +} + +int git_hash_sha1_init(git_hash_sha1_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + return hash_win32_init(&ctx->win32); +} + +int git_hash_sha1_update(git_hash_sha1_ctx *ctx, const void *data, size_t len) +{ + GIT_ASSERT_ARG(ctx); + return hash_win32_update(&ctx->win32, data, len); +} + +int git_hash_sha1_final(unsigned char *out, git_hash_sha1_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + return hash_win32_final(out, &ctx->win32); +} + +void git_hash_sha1_ctx_cleanup(git_hash_sha1_ctx *ctx) +{ + if (!ctx) + return; + hash_win32_cleanup(&ctx->win32); +} + +#endif + +#ifdef GIT_SHA256_WIN32 + +int git_hash_sha256_global_init(void) +{ + return hash_provider_init(); +} + +int git_hash_sha256_ctx_init(git_hash_sha256_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + return hash_sha256_win32_ctx_init(&ctx->win32); +} + +int git_hash_sha256_init(git_hash_sha256_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + return hash_win32_init(&ctx->win32); +} + +int git_hash_sha256_update(git_hash_sha256_ctx *ctx, const void *data, size_t len) +{ + GIT_ASSERT_ARG(ctx); + return hash_win32_update(&ctx->win32, data, len); +} + +int git_hash_sha256_final(unsigned char *out, git_hash_sha256_ctx *ctx) +{ + GIT_ASSERT_ARG(ctx); + return hash_win32_final(out, &ctx->win32); +} + +void git_hash_sha256_ctx_cleanup(git_hash_sha256_ctx *ctx) +{ + if (!ctx) + return; + hash_win32_cleanup(&ctx->win32); +} + +#endif diff --git a/src/util/hash/win32.h b/src/util/hash/win32.h new file mode 100644 index 0000000..a9fb87a --- /dev/null +++ b/src/util/hash/win32.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_hash_win32_h__ +#define INCLUDE_hash_win32_h__ + +#include "hash/sha.h" + +#include + +typedef enum { + GIT_HASH_WIN32_INVALID = 0, + GIT_HASH_WIN32_CRYPTOAPI, + GIT_HASH_WIN32_CNG +} git_hash_win32_provider_t; + +struct git_hash_win32_cryptoapi_ctx { + bool valid; + HCRYPTHASH hash_handle; +}; + +struct git_hash_win32_cng_ctx { + bool updated; + HANDLE /* BCRYPT_HASH_HANDLE */ hash_handle; + PBYTE hash_object; +}; + +typedef struct { + ALG_ID algorithm; + + union { + struct git_hash_win32_cryptoapi_ctx cryptoapi; + struct git_hash_win32_cng_ctx cng; + } ctx; +} git_hash_win32_ctx; + +/* + * Gets/sets the current hash provider (cng or cryptoapi). This is only + * for testing purposes. + */ +git_hash_win32_provider_t git_hash_win32_provider(void); +int git_hash_win32_set_provider(git_hash_win32_provider_t provider); + +#ifdef GIT_SHA1_WIN32 +struct git_hash_sha1_ctx { + git_hash_win32_ctx win32; +}; +#endif + +#ifdef GIT_SHA256_WIN32 +struct git_hash_sha256_ctx { + git_hash_win32_ctx win32; +}; +#endif + +#endif diff --git a/src/util/integer.h b/src/util/integer.h new file mode 100644 index 0000000..6327717 --- /dev/null +++ b/src/util/integer.h @@ -0,0 +1,218 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_integer_h__ +#define INCLUDE_integer_h__ + +/** @return true if p fits into the range of a size_t */ +GIT_INLINE(int) git__is_sizet(int64_t p) +{ + size_t r = (size_t)p; + return p == (int64_t)r; +} + +/** @return true if p fits into the range of an ssize_t */ +GIT_INLINE(int) git__is_ssizet(size_t p) +{ + ssize_t r = (ssize_t)p; + return p == (size_t)r; +} + +/** @return true if p fits into the range of a uint16_t */ +GIT_INLINE(int) git__is_uint16(size_t p) +{ + uint16_t r = (uint16_t)p; + return p == (size_t)r; +} + +/** @return true if p fits into the range of a uint32_t */ +GIT_INLINE(int) git__is_uint32(size_t p) +{ + uint32_t r = (uint32_t)p; + return p == (size_t)r; +} + +/** @return true if p fits into the range of an unsigned long */ +GIT_INLINE(int) git__is_ulong(int64_t p) +{ + unsigned long r = (unsigned long)p; + return p == (int64_t)r; +} + +/** @return true if p fits into the range of an int */ +GIT_INLINE(int) git__is_int(int64_t p) +{ + int r = (int)p; + return p == (int64_t)r; +} + +/* Use clang/gcc compiler intrinsics whenever possible */ +#if (__has_builtin(__builtin_add_overflow) || \ + (defined(__GNUC__) && (__GNUC__ >= 5))) + +# if (SIZE_MAX == UINT_MAX) +# define git__add_sizet_overflow(out, one, two) \ + __builtin_uadd_overflow(one, two, out) +# define git__multiply_sizet_overflow(out, one, two) \ + __builtin_umul_overflow(one, two, out) +# elif (SIZE_MAX == ULONG_MAX) +# define git__add_sizet_overflow(out, one, two) \ + __builtin_uaddl_overflow(one, two, out) +# define git__multiply_sizet_overflow(out, one, two) \ + __builtin_umull_overflow(one, two, out) +# elif (SIZE_MAX == ULLONG_MAX) +# define git__add_sizet_overflow(out, one, two) \ + __builtin_uaddll_overflow(one, two, out) +# define git__multiply_sizet_overflow(out, one, two) \ + __builtin_umulll_overflow(one, two, out) +# else +# error compiler has add with overflow intrinsics but SIZE_MAX is unknown +# endif + +# define git__add_int_overflow(out, one, two) \ + __builtin_sadd_overflow(one, two, out) +# define git__sub_int_overflow(out, one, two) \ + __builtin_ssub_overflow(one, two, out) + +# define git__add_int64_overflow(out, one, two) \ + __builtin_add_overflow(one, two, out) + +/* clang on 32-bit systems produces an undefined reference to `__mulodi4`. */ +# if !defined(__clang__) || !defined(GIT_ARCH_32) +# define git__multiply_int64_overflow(out, one, two) \ + __builtin_mul_overflow(one, two, out) +# endif + +/* Use Microsoft's safe integer handling functions where available */ +#elif defined(_MSC_VER) + +# define ENABLE_INTSAFE_SIGNED_FUNCTIONS +# include + +# define git__add_sizet_overflow(out, one, two) \ + (SizeTAdd(one, two, out) != S_OK) +# define git__multiply_sizet_overflow(out, one, two) \ + (SizeTMult(one, two, out) != S_OK) + +#define git__add_int_overflow(out, one, two) \ + (IntAdd(one, two, out) != S_OK) +#define git__sub_int_overflow(out, one, two) \ + (IntSub(one, two, out) != S_OK) + +#define git__add_int64_overflow(out, one, two) \ + (LongLongAdd(one, two, out) != S_OK) +#define git__multiply_int64_overflow(out, one, two) \ + (LongLongMult(one, two, out) != S_OK) + +#else + +/** + * Sets `one + two` into `out`, unless the arithmetic would overflow. + * @return false if the result fits in a `size_t`, true on overflow. + */ +GIT_INLINE(bool) git__add_sizet_overflow(size_t *out, size_t one, size_t two) +{ + if (SIZE_MAX - one < two) + return true; + *out = one + two; + return false; +} + +/** + * Sets `one * two` into `out`, unless the arithmetic would overflow. + * @return false if the result fits in a `size_t`, true on overflow. + */ +GIT_INLINE(bool) git__multiply_sizet_overflow(size_t *out, size_t one, size_t two) +{ + if (one && SIZE_MAX / one < two) + return true; + *out = one * two; + return false; +} + +GIT_INLINE(bool) git__add_int_overflow(int *out, int one, int two) +{ + if ((two > 0 && one > (INT_MAX - two)) || + (two < 0 && one < (INT_MIN - two))) + return true; + *out = one + two; + return false; +} + +GIT_INLINE(bool) git__sub_int_overflow(int *out, int one, int two) +{ + if ((two > 0 && one < (INT_MIN + two)) || + (two < 0 && one > (INT_MAX + two))) + return true; + *out = one - two; + return false; +} + +GIT_INLINE(bool) git__add_int64_overflow(int64_t *out, int64_t one, int64_t two) +{ + if ((two > 0 && one > (INT64_MAX - two)) || + (two < 0 && one < (INT64_MIN - two))) + return true; + *out = one + two; + return false; +} + +#endif + +/* If we could not provide an intrinsic implementation for this, provide a (slow) fallback. */ +#if !defined(git__multiply_int64_overflow) +GIT_INLINE(bool) git__multiply_int64_overflow(int64_t *out, int64_t one, int64_t two) +{ + /* + * Detects whether `INT64_MAX < (one * two) || INT64_MIN > (one * two)`, + * without incurring in undefined behavior. That is done by performing the + * comparison with a division instead of a multiplication, which translates + * to `INT64_MAX / one < two || INT64_MIN / one > two`. Some caveats: + * + * - The comparison sign is inverted when both sides of the inequality are + * multiplied/divided by a negative number, so if `one < 0` the comparison + * needs to be flipped. + * - `INT64_MAX / -1` itself overflows (or traps), so that case should be + * avoided. + * - Since the overflow flag is defined as the discrepance between the result + * of performing the multiplication in a signed integer at twice the width + * of the operands, and the truncated+sign-extended version of that same + * result, there are four cases where the result is the opposite of what + * would be expected: + * * `INT64_MIN * -1` / `-1 * INT64_MIN` + * * `INT64_MIN * 1 / `1 * INT64_MIN` + */ + if (one && two) { + if (one > 0 && two > 0) { + if (INT64_MAX / one < two) + return true; + } else if (one < 0 && two < 0) { + if ((one == -1 && two == INT64_MIN) || + (two == -1 && one == INT64_MIN)) { + *out = INT64_MIN; + return false; + } + if (INT64_MAX / one > two) + return true; + } else if (one > 0 && two < 0) { + if ((one == 1 && two == INT64_MIN) || + (INT64_MIN / one > two)) + return true; + } else if (one == -1) { + if (INT64_MIN / two > one) + return true; + } else { + if ((one == INT64_MIN && two == 1) || + (INT64_MIN / one < two)) + return true; + } + } + *out = one * two; + return false; +} +#endif + +#endif diff --git a/src/util/khash.h b/src/util/khash.h new file mode 100644 index 0000000..c9b7f13 --- /dev/null +++ b/src/util/khash.h @@ -0,0 +1,615 @@ +/* The MIT License + + Copyright (c) 2008, 2009, 2011 by Attractive Chaos + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +/* + An example: + +#include "khash.h" +KHASH_MAP_INIT_INT(32, char) +int main() { + int ret, is_missing; + khiter_t k; + khash_t(32) *h = kh_init(32); + k = kh_put(32, h, 5, &ret); + kh_value(h, k) = 10; + k = kh_get(32, h, 10); + is_missing = (k == kh_end(h)); + k = kh_get(32, h, 5); + kh_del(32, h, k); + for (k = kh_begin(h); k != kh_end(h); ++k) + if (kh_exist(h, k)) kh_value(h, k) = 1; + kh_destroy(32, h); + return 0; +} +*/ + +/* + 2013-05-02 (0.2.8): + + * Use quadratic probing. When the capacity is power of 2, stepping function + i*(i+1)/2 guarantees to traverse each bucket. It is better than double + hashing on cache performance and is more robust than linear probing. + + In theory, double hashing should be more robust than quadratic probing. + However, my implementation is probably not for large hash tables, because + the second hash function is closely tied to the first hash function, + which reduce the effectiveness of double hashing. + + Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php + + 2011-12-29 (0.2.7): + + * Minor code clean up; no actual effect. + + 2011-09-16 (0.2.6): + + * The capacity is a power of 2. This seems to dramatically improve the + speed for simple keys. Thank Zilong Tan for the suggestion. Reference: + + - http://code.google.com/p/ulib/ + - http://nothings.org/computer/judy/ + + * Allow to optionally use linear probing which usually has better + performance for random input. Double hashing is still the default as it + is more robust to certain non-random input. + + * Added Wang's integer hash function (not used by default). This hash + function is more robust to certain non-random input. + + 2011-02-14 (0.2.5): + + * Allow to declare global functions. + + 2009-09-26 (0.2.4): + + * Improve portability + + 2008-09-19 (0.2.3): + + * Corrected the example + * Improved interfaces + + 2008-09-11 (0.2.2): + + * Improved speed a little in kh_put() + + 2008-09-10 (0.2.1): + + * Added kh_clear() + * Fixed a compiling error + + 2008-09-02 (0.2.0): + + * Changed to token concatenation which increases flexibility. + + 2008-08-31 (0.1.2): + + * Fixed a bug in kh_get(), which has not been tested previously. + + 2008-08-31 (0.1.1): + + * Added destructor +*/ + + +#ifndef __AC_KHASH_H +#define __AC_KHASH_H + +/*! + @header + + Generic hash table library. + */ + +#define AC_VERSION_KHASH_H "0.2.8" + +#include +#include +#include + +/* compiler specific configuration */ + +typedef uint32_t khint32_t; +typedef uint64_t khint64_t; + +#ifndef kh_inline +#ifdef _MSC_VER +#define kh_inline __inline +#elif defined(__GNUC__) +#define kh_inline __inline__ +#else +#define kh_inline +#endif +#endif /* kh_inline */ + +typedef khint32_t khint_t; +typedef khint_t khiter_t; + +#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2) +#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1) +#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3) +#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1))) +#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1))) +#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1))) +#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1)) + +#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4) + +#ifndef kroundup32 +#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) +#endif + +#ifndef kcalloc +#define kcalloc(N,Z) calloc(N,Z) +#endif +#ifndef kmalloc +#define kmalloc(Z) malloc(Z) +#endif +#ifndef krealloc +#define krealloc(P,Z) realloc(P,Z) +#endif +#ifndef kreallocarray +#define kreallocarray(P,N,Z) ((SIZE_MAX - N < Z) ? NULL : krealloc(P, (N*Z))) +#endif +#ifndef kfree +#define kfree(P) free(P) +#endif + +static const double __ac_HASH_UPPER = 0.77; + +#define __KHASH_TYPE(name, khkey_t, khval_t) \ + typedef struct kh_##name##_s { \ + khint_t n_buckets, size, n_occupied, upper_bound; \ + khint32_t *flags; \ + khkey_t *keys; \ + khval_t *vals; \ + } kh_##name##_t; + +#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ + extern kh_##name##_t *kh_init_##name(void); \ + extern void kh_destroy_##name(kh_##name##_t *h); \ + extern void kh_clear_##name(kh_##name##_t *h); \ + extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ + extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ + extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ + extern void kh_del_##name(kh_##name##_t *h, khint_t x); + +#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + SCOPE kh_##name##_t *kh_init_##name(void) { \ + return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ + } \ + SCOPE void kh_destroy_##name(kh_##name##_t *h) \ + { \ + if (h) { \ + kfree((void *)h->keys); kfree(h->flags); \ + kfree((void *)h->vals); \ + kfree(h); \ + } \ + } \ + SCOPE void kh_clear_##name(kh_##name##_t *h) \ + { \ + if (h && h->flags) { \ + memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ + h->size = h->n_occupied = 0; \ + } \ + } \ + SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ + { \ + if (h->n_buckets) { \ + khint_t k, i, last, mask, step = 0; \ + mask = h->n_buckets - 1; \ + k = __hash_func(key); i = k & mask; \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + i = (i + (++step)) & mask; \ + if (i == last) return h->n_buckets; \ + } \ + return __ac_iseither(h->flags, i)? h->n_buckets : i; \ + } else return 0; \ + } \ + SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ + { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \ + khint32_t *new_flags = 0; \ + khint_t j = 1; \ + { \ + kroundup32(new_n_buckets); \ + if (new_n_buckets < 4) new_n_buckets = 4; \ + if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \ + else { /* hash table size to be changed (shrink or expand); rehash */ \ + new_flags = (khint32_t*)kreallocarray(NULL, __ac_fsize(new_n_buckets), sizeof(khint32_t)); \ + if (!new_flags) return -1; \ + memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (h->n_buckets < new_n_buckets) { /* expand */ \ + khkey_t *new_keys = (khkey_t*)kreallocarray((void *)h->keys, new_n_buckets, sizeof(khkey_t)); \ + if (!new_keys) { kfree(new_flags); return -1; } \ + h->keys = new_keys; \ + if (kh_is_map) { \ + khval_t *new_vals = (khval_t*)kreallocarray((void *)h->vals, new_n_buckets, sizeof(khval_t)); \ + if (!new_vals) { kfree(new_flags); return -1; } \ + h->vals = new_vals; \ + } \ + } /* otherwise shrink */ \ + } \ + } \ + if (j) { /* rehashing is needed */ \ + for (j = 0; j != h->n_buckets; ++j) { \ + if (__ac_iseither(h->flags, j) == 0) { \ + khkey_t key = h->keys[j]; \ + khval_t val; \ + khint_t new_mask; \ + new_mask = new_n_buckets - 1; \ + if (kh_is_map) val = h->vals[j]; \ + __ac_set_isdel_true(h->flags, j); \ + while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ + khint_t k, i, step = 0; \ + k = __hash_func(key); \ + i = k & new_mask; \ + while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \ + __ac_set_isempty_false(new_flags, i); \ + if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \ + { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ + if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \ + __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \ + } else { /* write the element and jump out of the loop */ \ + h->keys[i] = key; \ + if (kh_is_map) h->vals[i] = val; \ + break; \ + } \ + } \ + } \ + } \ + if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ + h->keys = (khkey_t*)kreallocarray((void *)h->keys, new_n_buckets, sizeof(khkey_t)); \ + if (kh_is_map) h->vals = (khval_t*)kreallocarray((void *)h->vals, new_n_buckets, sizeof(khval_t)); \ + } \ + kfree(h->flags); /* free the working space */ \ + h->flags = new_flags; \ + h->n_buckets = new_n_buckets; \ + h->n_occupied = h->size; \ + h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ + } \ + return 0; \ + } \ + SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ + { \ + khint_t x; \ + if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ + if (h->n_buckets > (h->size<<1)) { \ + if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \ + *ret = -1; return h->n_buckets; \ + } \ + } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \ + *ret = -1; return h->n_buckets; \ + } \ + } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ + { \ + khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ + x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \ + if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \ + else { \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + if (__ac_isdel(h->flags, i)) site = i; \ + i = (i + (++step)) & mask; \ + if (i == last) { x = site; break; } \ + } \ + if (x == h->n_buckets) { \ + if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ + else x = i; \ + } \ + } \ + } \ + if (__ac_isempty(h->flags, x)) { /* not present at all */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; ++h->n_occupied; \ + *ret = 1; \ + } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; \ + *ret = 2; \ + } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ + return x; \ + } \ + SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ + { \ + if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ + __ac_set_isdel_true(h->flags, x); \ + --h->size; \ + } \ + } + +#define KHASH_DECLARE(name, khkey_t, khval_t) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_PROTOTYPES(name, khkey_t, khval_t) + +#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + KHASH_INIT2(name, static kh_inline, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +/* --- BEGIN OF HASH FUNCTIONS --- */ + +/*! @function + @abstract Integer hash function + @param key The integer [khint32_t] + @return The hash value [khint_t] + */ +#define kh_int_hash_func(key) (khint32_t)(key) +/*! @function + @abstract Integer comparison function + */ +#define kh_int_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract 64-bit integer hash function + @param key The integer [khint64_t] + @return The hash value [khint_t] + */ +#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11) +/*! @function + @abstract 64-bit integer comparison function + */ +#define kh_int64_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract const char* hash function + @param s Pointer to a null terminated string + @return The hash value + */ +static kh_inline khint_t __ac_X31_hash_string(const char *s) +{ + khint_t h = (khint_t)*s; + if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s; + return h; +} +/*! @function + @abstract Another interface to const char* hash function + @param key Pointer to a null terminated string [const char*] + @return The hash value [khint_t] + */ +#define kh_str_hash_func(key) __ac_X31_hash_string(key) +/*! @function + @abstract Const char* comparison function + */ +#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0) + +static kh_inline khint_t __ac_Wang_hash(khint_t key) +{ + key += ~(key << 15); + key ^= (key >> 10); + key += (key << 3); + key ^= (key >> 6); + key += ~(key << 11); + key ^= (key >> 16); + return key; +} +#define kh_int_hash_func2(k) __ac_Wang_hash((khint_t)key) + +/* --- END OF HASH FUNCTIONS --- */ + +/* Other convenient macros... */ + +/*! + @abstract Type of the hash table. + @param name Name of the hash table [symbol] + */ +#define khash_t(name) kh_##name##_t + +/*! @function + @abstract Initiate a hash table. + @param name Name of the hash table [symbol] + @return Pointer to the hash table [khash_t(name)*] + */ +#define kh_init(name) kh_init_##name() + +/*! @function + @abstract Destroy a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_destroy(name, h) kh_destroy_##name(h) + +/*! @function + @abstract Reset a hash table without deallocating memory. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_clear(name, h) kh_clear_##name(h) + +/*! @function + @abstract Resize a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param s New size [khint_t] + */ +#define kh_resize(name, h, s) kh_resize_##name(h, s) + +/*! @function + @abstract Insert a key to the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @param r Extra return code: -1 if the operation failed; + 0 if the key is present in the hash table; + 1 if the bucket is empty (never used); 2 if the element in + the bucket has been deleted [int*] + @return Iterator to the inserted element [khint_t] + */ +#define kh_put(name, h, k, r) kh_put_##name(h, k, r) + +/*! @function + @abstract Retrieve a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t] + */ +#define kh_get(name, h, k) kh_get_##name(h, k) + +/*! @function + @abstract Remove a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Iterator to the element to be deleted [khint_t] + */ +#define kh_del(name, h, k) kh_del_##name(h, k) + +/*! @function + @abstract Test whether a bucket contains data. + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return 1 if containing data; 0 otherwise [int] + */ +#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x))) + +/*! @function + @abstract Get key given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Key [type of keys] + */ +#define kh_key(h, x) ((h)->keys[x]) + +/*! @function + @abstract Get value given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Value [type of values] + @discussion For hash sets, calling this results in segfault. + */ +#define kh_val(h, x) ((h)->vals[x]) + +/*! @function + @abstract Alias of kh_val() + */ +#define kh_value(h, x) ((h)->vals[x]) + +/*! @function + @abstract Get the start iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The start iterator [khint_t] + */ +#define kh_begin(h) (khint_t)(0) + +/*! @function + @abstract Get the end iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The end iterator [khint_t] + */ +#define kh_end(h) ((h)->n_buckets) + +/*! @function + @abstract Get the number of elements in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of elements in the hash table [khint_t] + */ +#define kh_size(h) ((h)->size) + +/*! @function + @abstract Get the number of buckets in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of buckets in the hash table [khint_t] + */ +#define kh_n_buckets(h) ((h)->n_buckets) + +/*! @function + @abstract Iterate over the entries in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param kvar Variable to which key will be assigned + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (kvar) = kh_key(h,__i); \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/*! @function + @abstract Iterate over the values in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach_value(h, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/* More convenient interfaces */ + +/*! @function + @abstract Instantiate a hash set containing integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT(name) \ + KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT(name, khval_t) \ + KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing 64-bit integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT64(name) \ + KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing 64-bit integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT64(name, khval_t) \ + KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal) + +typedef const char *kh_cstr_t; +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_STR(name) \ + KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_STR(name, khval_t) \ + KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) + +#endif /* __AC_KHASH_H */ diff --git a/src/util/map.h b/src/util/map.h new file mode 100644 index 0000000..c101e46 --- /dev/null +++ b/src/util/map.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_map_h__ +#define INCLUDE_map_h__ + +#include "git2_util.h" + + +/* p_mmap() prot values */ +#define GIT_PROT_NONE 0x0 +#define GIT_PROT_READ 0x1 +#define GIT_PROT_WRITE 0x2 +#define GIT_PROT_EXEC 0x4 + +/* git__mmmap() flags values */ +#define GIT_MAP_FILE 0 +#define GIT_MAP_SHARED 1 +#define GIT_MAP_PRIVATE 2 +#define GIT_MAP_TYPE 0xf +#define GIT_MAP_FIXED 0x10 + +#ifdef __amigaos4__ +#define MAP_FAILED 0 +#endif + +typedef struct { /* memory mapped buffer */ + void *data; /* data bytes */ + size_t len; /* data length */ +#ifdef GIT_WIN32 + HANDLE fmh; /* file mapping handle */ +#endif +} git_map; + +#define GIT_MMAP_VALIDATE(out, len, prot, flags) do { \ + GIT_ASSERT(out != NULL && len > 0); \ + GIT_ASSERT((prot & GIT_PROT_WRITE) || (prot & GIT_PROT_READ)); \ + GIT_ASSERT((flags & GIT_MAP_FIXED) == 0); } while (0) + +extern int p_mmap(git_map *out, size_t len, int prot, int flags, int fd, off64_t offset); +extern int p_munmap(git_map *map); + +#endif diff --git a/src/util/net.c b/src/util/net.c new file mode 100644 index 0000000..afd52ce --- /dev/null +++ b/src/util/net.c @@ -0,0 +1,1154 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "net.h" + +#include + +#include "posix.h" +#include "str.h" +#include "http_parser.h" +#include "runtime.h" + +#define DEFAULT_PORT_HTTP "80" +#define DEFAULT_PORT_HTTPS "443" +#define DEFAULT_PORT_GIT "9418" +#define DEFAULT_PORT_SSH "22" + +#define GIT_NET_URL_PARSER_INIT { 0 } + +typedef struct { + int hierarchical : 1; + + const char *scheme; + const char *user; + const char *password; + const char *host; + const char *port; + const char *path; + const char *query; + const char *fragment; + + size_t scheme_len; + size_t user_len; + size_t password_len; + size_t host_len; + size_t port_len; + size_t path_len; + size_t query_len; + size_t fragment_len; +} git_net_url_parser; + +bool git_net_hostname_matches_cert( + const char *hostname, + const char *pattern) +{ + for (;;) { + char c = git__tolower(*pattern++); + + if (c == '\0') + return *hostname ? false : true; + + if (c == '*') { + c = *pattern; + + /* '*' at the end matches everything left */ + if (c == '\0') + return true; + + /* + * We've found a pattern, so move towards the + * next matching char. The '.' is handled + * specially because wildcards aren't allowed + * to cross subdomains. + */ + while(*hostname) { + char h = git__tolower(*hostname); + + if (h == c) + return git_net_hostname_matches_cert(hostname++, pattern); + else if (h == '.') + return git_net_hostname_matches_cert(hostname, pattern); + + hostname++; + } + + return false; + } + + if (c != git__tolower(*hostname++)) + return false; + } + + return false; +} + +#define is_valid_scheme_char(c) \ + (((c) >= 'a' && (c) <= 'z') || \ + ((c) >= 'A' && (c) <= 'Z') || \ + ((c) >= '0' && (c) <= '9') || \ + (c) == '+' || (c) == '-' || (c) == '.') + +bool git_net_str_is_url(const char *str) +{ + const char *c; + + for (c = str; *c; c++) { + if (*c == ':' && *(c+1) == '/' && *(c+2) == '/') + return true; + + if (!is_valid_scheme_char(*c)) + break; + } + + return false; +} + +static const char *default_port_for_scheme(const char *scheme) +{ + if (strcmp(scheme, "http") == 0) + return DEFAULT_PORT_HTTP; + else if (strcmp(scheme, "https") == 0) + return DEFAULT_PORT_HTTPS; + else if (strcmp(scheme, "git") == 0) + return DEFAULT_PORT_GIT; + else if (strcmp(scheme, "ssh") == 0 || + strcmp(scheme, "ssh+git") == 0 || + strcmp(scheme, "git+ssh") == 0) + return DEFAULT_PORT_SSH; + + return NULL; +} + +static bool is_ssh_scheme(const char *scheme, size_t scheme_len) +{ + if (!scheme_len) + return false; + + return strncasecmp(scheme, "ssh", scheme_len) == 0 || + strncasecmp(scheme, "ssh+git", scheme_len) == 0 || + strncasecmp(scheme, "git+ssh", scheme_len) == 0; +} + +int git_net_url_dup(git_net_url *out, git_net_url *in) +{ + if (in->scheme) { + out->scheme = git__strdup(in->scheme); + GIT_ERROR_CHECK_ALLOC(out->scheme); + } + + if (in->host) { + out->host = git__strdup(in->host); + GIT_ERROR_CHECK_ALLOC(out->host); + } + + if (in->port) { + out->port = git__strdup(in->port); + GIT_ERROR_CHECK_ALLOC(out->port); + } + + if (in->path) { + out->path = git__strdup(in->path); + GIT_ERROR_CHECK_ALLOC(out->path); + } + + if (in->query) { + out->query = git__strdup(in->query); + GIT_ERROR_CHECK_ALLOC(out->query); + } + + if (in->username) { + out->username = git__strdup(in->username); + GIT_ERROR_CHECK_ALLOC(out->username); + } + + if (in->password) { + out->password = git__strdup(in->password); + GIT_ERROR_CHECK_ALLOC(out->password); + } + + return 0; +} + +static int url_invalid(const char *message) +{ + git_error_set(GIT_ERROR_NET, "invalid url: %s", message); + return GIT_EINVALIDSPEC; +} + +static int url_parse_authority( + git_net_url_parser *parser, + const char *authority, + size_t len) +{ + const char *c, *hostport_end, *host_end = NULL, + *userpass_end, *user_end = NULL; + + enum { + HOSTPORT, HOST, IPV6, HOST_END, USERPASS, USER + } state = HOSTPORT; + + if (len == 0) + return 0; + + /* + * walk the authority backwards so that we can parse google code's + * ssh urls that are not rfc compliant and allow @ in the username + */ + for (hostport_end = authority + len, c = hostport_end - 1; + c >= authority && !user_end; + c--) { + switch (state) { + case HOSTPORT: + if (*c == ':') { + parser->port = c + 1; + parser->port_len = hostport_end - parser->port; + host_end = c; + state = HOST; + break; + } + + /* + * if we've only seen digits then we don't know + * if we're parsing just a host or a host and port. + * if we see a non-digit, then we're in a host, + * otherwise, fall through to possibly match the + * "@" (user/host separator). + */ + + if (*c < '0' || *c > '9') { + host_end = hostport_end; + state = HOST; + } + + /* fall through */ + + case HOST: + if (*c == ']' && host_end == c + 1) { + host_end = c; + state = IPV6; + } + + else if (*c == '@') { + parser->host = c + 1; + parser->host_len = host_end ? + host_end - parser->host : + hostport_end - parser->host; + userpass_end = c; + state = USERPASS; + } + + else if (*c == '[' || *c == ']' || *c == ':') { + return url_invalid("malformed hostname"); + } + + break; + + case IPV6: + if (*c == '[') { + parser->host = c + 1; + parser->host_len = host_end - parser->host; + state = HOST_END; + } + + else if ((*c < '0' || *c > '9') && + (*c < 'a' || *c > 'f') && + (*c < 'A' || *c > 'F') && + (*c != ':')) { + return url_invalid("malformed hostname"); + } + + break; + + case HOST_END: + if (*c == '@') { + userpass_end = c; + state = USERPASS; + break; + } + + return url_invalid("malformed hostname"); + + case USERPASS: + if (*c == '@' && + !is_ssh_scheme(parser->scheme, parser->scheme_len)) + return url_invalid("malformed hostname"); + + if (*c == ':') { + parser->password = c + 1; + parser->password_len = userpass_end - parser->password; + user_end = c; + state = USER; + break; + } + + break; + + default: + GIT_ASSERT(!"unhandled state"); + } + } + + switch (state) { + case HOSTPORT: + parser->host = authority; + parser->host_len = (hostport_end - parser->host); + break; + case HOST: + parser->host = authority; + parser->host_len = (host_end - parser->host); + break; + case IPV6: + return url_invalid("malformed hostname"); + case HOST_END: + break; + case USERPASS: + parser->user = authority; + parser->user_len = (userpass_end - parser->user); + break; + case USER: + parser->user = authority; + parser->user_len = (user_end - parser->user); + break; + default: + GIT_ASSERT(!"unhandled state"); + } + + return 0; +} + +static int url_parse_path( + git_net_url_parser *parser, + const char *path, + size_t len) +{ + const char *c, *end; + + enum { PATH, QUERY, FRAGMENT } state = PATH; + + parser->path = path; + end = path + len; + + for (c = path; c < end; c++) { + switch (state) { + case PATH: + switch (*c) { + case '?': + parser->path_len = (c - parser->path); + parser->query = c + 1; + state = QUERY; + break; + case '#': + parser->path_len = (c - parser->path); + parser->fragment = c + 1; + state = FRAGMENT; + break; + } + break; + + case QUERY: + if (*c == '#') { + parser->query_len = (c - parser->query); + parser->fragment = c + 1; + state = FRAGMENT; + } + break; + + case FRAGMENT: + break; + + default: + GIT_ASSERT(!"unhandled state"); + } + } + + switch (state) { + case PATH: + parser->path_len = (c - parser->path); + break; + case QUERY: + parser->query_len = (c - parser->query); + break; + case FRAGMENT: + parser->fragment_len = (c - parser->fragment); + break; + } + + return 0; +} + +static int url_parse_finalize(git_net_url *url, git_net_url_parser *parser) +{ + git_str scheme = GIT_STR_INIT, user = GIT_STR_INIT, + password = GIT_STR_INIT, host = GIT_STR_INIT, + port = GIT_STR_INIT, path = GIT_STR_INIT, + query = GIT_STR_INIT, fragment = GIT_STR_INIT; + const char *default_port; + int error = 0; + + if (parser->scheme_len) { + if ((error = git_str_put(&scheme, parser->scheme, parser->scheme_len)) < 0) + goto done; + + git__strntolower(scheme.ptr, scheme.size); + } + + if (parser->user_len && + (error = git_str_decode_percent(&user, parser->user, parser->user_len)) < 0) + goto done; + + if (parser->password_len && + (error = git_str_decode_percent(&password, parser->password, parser->password_len)) < 0) + goto done; + + if (parser->host_len && + (error = git_str_decode_percent(&host, parser->host, parser->host_len)) < 0) + goto done; + + if (parser->port_len) + error = git_str_put(&port, parser->port, parser->port_len); + else if (parser->scheme_len && (default_port = default_port_for_scheme(scheme.ptr)) != NULL) + error = git_str_puts(&port, default_port); + + if (error < 0) + goto done; + + if (parser->path_len) + error = git_str_put(&path, parser->path, parser->path_len); + else if (parser->hierarchical) + error = git_str_puts(&path, "/"); + + if (error < 0) + goto done; + + if (parser->query_len && + (error = git_str_decode_percent(&query, parser->query, parser->query_len)) < 0) + goto done; + + if (parser->fragment_len && + (error = git_str_decode_percent(&fragment, parser->fragment, parser->fragment_len)) < 0) + goto done; + + url->scheme = git_str_detach(&scheme); + url->host = git_str_detach(&host); + url->port = git_str_detach(&port); + url->path = git_str_detach(&path); + url->query = git_str_detach(&query); + url->fragment = git_str_detach(&fragment); + url->username = git_str_detach(&user); + url->password = git_str_detach(&password); + + error = 0; + +done: + git_str_dispose(&scheme); + git_str_dispose(&user); + git_str_dispose(&password); + git_str_dispose(&host); + git_str_dispose(&port); + git_str_dispose(&path); + git_str_dispose(&query); + git_str_dispose(&fragment); + + return error; +} + +int git_net_url_parse(git_net_url *url, const char *given) +{ + git_net_url_parser parser = GIT_NET_URL_PARSER_INIT; + const char *c, *authority, *path; + size_t authority_len = 0, path_len = 0; + int error = 0; + + enum { + SCHEME_START, SCHEME, + AUTHORITY_START, AUTHORITY, + PATH_START, PATH + } state = SCHEME_START; + + memset(url, 0, sizeof(git_net_url)); + + for (c = given; *c; c++) { + switch (state) { + case SCHEME_START: + parser.scheme = c; + state = SCHEME; + + /* fall through */ + + case SCHEME: + if (*c == ':') { + parser.scheme_len = (c - parser.scheme); + + if (parser.scheme_len && + *(c+1) == '/' && *(c+2) == '/') { + c += 2; + parser.hierarchical = 1; + state = AUTHORITY_START; + } else { + state = PATH_START; + } + } else if (!is_valid_scheme_char(*c)) { + /* + * an illegal scheme character means that we + * were just given a relative path + */ + path = given; + state = PATH; + break; + } + break; + + case AUTHORITY_START: + authority = c; + state = AUTHORITY; + + /* fall through */ + case AUTHORITY: + if (*c != '/') + break; + + authority_len = (c - authority); + + /* fall through */ + case PATH_START: + path = c; + state = PATH; + break; + + case PATH: + break; + + default: + GIT_ASSERT(!"unhandled state"); + } + } + + switch (state) { + case SCHEME: + /* + * if we never saw a ':' then we were given a relative + * path, not a bare scheme + */ + path = given; + path_len = (c - path); + break; + case AUTHORITY_START: + break; + case AUTHORITY: + authority_len = (c - authority); + break; + case PATH_START: + break; + case PATH: + path_len = (c - path); + break; + default: + GIT_ASSERT(!"unhandled state"); + } + + if (authority_len && + (error = url_parse_authority(&parser, authority, authority_len)) < 0) + goto done; + + if (path_len && + (error = url_parse_path(&parser, path, path_len)) < 0) + goto done; + + error = url_parse_finalize(url, &parser); + +done: + return error; +} + +int git_net_url_parse_http( + git_net_url *url, + const char *given) +{ + git_net_url_parser parser = GIT_NET_URL_PARSER_INIT; + const char *c, *authority, *path = NULL; + size_t authority_len = 0, path_len = 0; + int error; + + /* Hopefully this is a proper URL with a scheme. */ + if (git_net_str_is_url(given)) + return git_net_url_parse(url, given); + + memset(url, 0, sizeof(git_net_url)); + + /* Without a scheme, we are in the host (authority) section. */ + for (c = authority = given; *c; c++) { + if (!path && *c == '/') { + authority_len = (c - authority); + path = c; + } + } + + if (path) + path_len = (c - path); + else + authority_len = (c - authority); + + parser.scheme = "http"; + parser.scheme_len = 4; + parser.hierarchical = 1; + + if (authority_len && + (error = url_parse_authority(&parser, authority, authority_len)) < 0) + return error; + + if (path_len && + (error = url_parse_path(&parser, path, path_len)) < 0) + return error; + + return url_parse_finalize(url, &parser); +} + +static int scp_invalid(const char *message) +{ + git_error_set(GIT_ERROR_NET, "invalid scp-style path: %s", message); + return GIT_EINVALIDSPEC; +} + +static bool is_ipv6(const char *str) +{ + const char *c; + size_t colons = 0; + + if (*str++ != '[') + return false; + + for (c = str; *c; c++) { + if (*c == ':') + colons++; + + if (*c == ']') + return (colons > 1); + + if (*c != ':' && + (*c < '0' || *c > '9') && + (*c < 'a' || *c > 'f') && + (*c < 'A' || *c > 'F')) + return false; + } + + return false; +} + +static bool has_at(const char *str) +{ + const char *c; + + for (c = str; *c; c++) { + if (*c == '@') + return true; + + if (*c == ':') + break; + } + + return false; +} + +int git_net_url_parse_scp(git_net_url *url, const char *given) +{ + const char *default_port = default_port_for_scheme("ssh"); + const char *c, *user, *host, *port, *path = NULL; + size_t user_len = 0, host_len = 0, port_len = 0; + unsigned short bracket = 0; + + enum { + NONE, + USER, + HOST_START, HOST, HOST_END, + IPV6, IPV6_END, + PORT_START, PORT, PORT_END, + PATH_START + } state = NONE; + + memset(url, 0, sizeof(git_net_url)); + + for (c = given; *c && !path; c++) { + switch (state) { + case NONE: + switch (*c) { + case '@': + return scp_invalid("unexpected '@'"); + case ':': + return scp_invalid("unexpected ':'"); + case '[': + if (is_ipv6(c)) { + state = IPV6; + host = c; + } else if (bracket++ > 1) { + return scp_invalid("unexpected '['"); + } + break; + default: + if (has_at(c)) { + state = USER; + user = c; + } else { + state = HOST; + host = c; + } + break; + } + break; + + case USER: + if (*c == '@') { + user_len = (c - user); + state = HOST_START; + } + break; + + case HOST_START: + state = (*c == '[') ? IPV6 : HOST; + host = c; + break; + + case HOST: + if (*c == ':') { + host_len = (c - host); + state = bracket ? PORT_START : PATH_START; + } else if (*c == ']') { + if (bracket-- == 0) + return scp_invalid("unexpected ']'"); + + host_len = (c - host); + state = HOST_END; + } + break; + + case HOST_END: + if (*c != ':') + return scp_invalid("unexpected character after hostname"); + state = PATH_START; + break; + + case IPV6: + if (*c == ']') + state = IPV6_END; + break; + + case IPV6_END: + if (*c != ':') + return scp_invalid("unexpected character after ipv6 address"); + + host_len = (c - host); + state = bracket ? PORT_START : PATH_START; + break; + + case PORT_START: + port = c; + state = PORT; + break; + + case PORT: + if (*c == ']') { + if (bracket-- == 0) + return scp_invalid("unexpected ']'"); + + port_len = c - port; + state = PORT_END; + } + break; + + case PORT_END: + if (*c != ':') + return scp_invalid("unexpected character after ipv6 address"); + + state = PATH_START; + break; + + case PATH_START: + path = c; + break; + + default: + GIT_ASSERT(!"unhandled state"); + } + } + + if (!path) + return scp_invalid("path is required"); + + GIT_ERROR_CHECK_ALLOC(url->scheme = git__strdup("ssh")); + + if (user_len) + GIT_ERROR_CHECK_ALLOC(url->username = git__strndup(user, user_len)); + + GIT_ASSERT(host_len); + GIT_ERROR_CHECK_ALLOC(url->host = git__strndup(host, host_len)); + + if (port_len) + GIT_ERROR_CHECK_ALLOC(url->port = git__strndup(port, port_len)); + else + GIT_ERROR_CHECK_ALLOC(url->port = git__strdup(default_port)); + + GIT_ASSERT(path); + GIT_ERROR_CHECK_ALLOC(url->path = git__strdup(path)); + + return 0; +} + +int git_net_url_parse_standard_or_scp(git_net_url *url, const char *given) +{ + return git_net_str_is_url(given) ? + git_net_url_parse(url, given) : + git_net_url_parse_scp(url, given); +} + +int git_net_url_joinpath( + git_net_url *out, + git_net_url *one, + const char *two) +{ + git_str path = GIT_STR_INIT; + const char *query; + size_t one_len, two_len; + + git_net_url_dispose(out); + + if ((query = strchr(two, '?')) != NULL) { + two_len = query - two; + + if (*(++query) != '\0') { + out->query = git__strdup(query); + GIT_ERROR_CHECK_ALLOC(out->query); + } + } else { + two_len = strlen(two); + } + + /* Strip all trailing `/`s from the first path */ + one_len = one->path ? strlen(one->path) : 0; + while (one_len && one->path[one_len - 1] == '/') + one_len--; + + /* Strip all leading `/`s from the second path */ + while (*two == '/') { + two++; + two_len--; + } + + git_str_put(&path, one->path, one_len); + git_str_putc(&path, '/'); + git_str_put(&path, two, two_len); + + if (git_str_oom(&path)) + return -1; + + out->path = git_str_detach(&path); + + if (one->scheme) { + out->scheme = git__strdup(one->scheme); + GIT_ERROR_CHECK_ALLOC(out->scheme); + } + + if (one->host) { + out->host = git__strdup(one->host); + GIT_ERROR_CHECK_ALLOC(out->host); + } + + if (one->port) { + out->port = git__strdup(one->port); + GIT_ERROR_CHECK_ALLOC(out->port); + } + + if (one->username) { + out->username = git__strdup(one->username); + GIT_ERROR_CHECK_ALLOC(out->username); + } + + if (one->password) { + out->password = git__strdup(one->password); + GIT_ERROR_CHECK_ALLOC(out->password); + } + + return 0; +} + +/* + * Some servers strip the query parameters from the Location header + * when sending a redirect. Others leave it in place. + * Check for both, starting with the stripped case first, + * since it appears to be more common. + */ +static void remove_service_suffix( + git_net_url *url, + const char *service_suffix) +{ + const char *service_query = strchr(service_suffix, '?'); + size_t full_suffix_len = strlen(service_suffix); + size_t suffix_len = service_query ? + (size_t)(service_query - service_suffix) : full_suffix_len; + size_t path_len = strlen(url->path); + ssize_t truncate = -1; + + /* + * Check for a redirect without query parameters, + * like "/newloc/info/refs"' + */ + if (suffix_len && path_len >= suffix_len) { + size_t suffix_offset = path_len - suffix_len; + + if (git__strncmp(url->path + suffix_offset, service_suffix, suffix_len) == 0 && + (!service_query || git__strcmp(url->query, service_query + 1) == 0)) { + truncate = suffix_offset; + } + } + + /* + * If we haven't already found where to truncate to remove the + * suffix, check for a redirect with query parameters, like + * "/newloc/info/refs?service=git-upload-pack" + */ + if (truncate < 0 && git__suffixcmp(url->path, service_suffix) == 0) + truncate = path_len - full_suffix_len; + + /* Ensure we leave a minimum of '/' as the path */ + if (truncate == 0) + truncate++; + + if (truncate > 0) { + url->path[truncate] = '\0'; + + git__free(url->query); + url->query = NULL; + } +} + +int git_net_url_apply_redirect( + git_net_url *url, + const char *redirect_location, + bool allow_offsite, + const char *service_suffix) +{ + git_net_url tmp = GIT_NET_URL_INIT; + int error = 0; + + GIT_ASSERT(url); + GIT_ASSERT(redirect_location); + + if (redirect_location[0] == '/') { + git__free(url->path); + + if ((url->path = git__strdup(redirect_location)) == NULL) { + error = -1; + goto done; + } + } else { + git_net_url *original = url; + + if ((error = git_net_url_parse(&tmp, redirect_location)) < 0) + goto done; + + /* Validate that this is a legal redirection */ + + if (original->scheme && + strcmp(original->scheme, tmp.scheme) != 0 && + strcmp(tmp.scheme, "https") != 0) { + git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'", + original->scheme, tmp.scheme); + + error = -1; + goto done; + } + + if (original->host && + !allow_offsite && + git__strcasecmp(original->host, tmp.host) != 0) { + git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'", + original->host, tmp.host); + + error = -1; + goto done; + } + + git_net_url_swap(url, &tmp); + } + + /* Remove the service suffix if it was given to us */ + if (service_suffix) + remove_service_suffix(url, service_suffix); + +done: + git_net_url_dispose(&tmp); + return error; +} + +bool git_net_url_valid(git_net_url *url) +{ + return (url->host && url->port && url->path); +} + +bool git_net_url_is_default_port(git_net_url *url) +{ + const char *default_port; + + if (url->scheme && (default_port = default_port_for_scheme(url->scheme)) != NULL) + return (strcmp(url->port, default_port) == 0); + else + return false; +} + +bool git_net_url_is_ipv6(git_net_url *url) +{ + return (strchr(url->host, ':') != NULL); +} + +void git_net_url_swap(git_net_url *a, git_net_url *b) +{ + git_net_url tmp = GIT_NET_URL_INIT; + + memcpy(&tmp, a, sizeof(git_net_url)); + memcpy(a, b, sizeof(git_net_url)); + memcpy(b, &tmp, sizeof(git_net_url)); +} + +int git_net_url_fmt(git_str *buf, git_net_url *url) +{ + GIT_ASSERT_ARG(url); + GIT_ASSERT_ARG(url->scheme); + GIT_ASSERT_ARG(url->host); + + git_str_puts(buf, url->scheme); + git_str_puts(buf, "://"); + + if (url->username) { + git_str_puts(buf, url->username); + + if (url->password) { + git_str_puts(buf, ":"); + git_str_puts(buf, url->password); + } + + git_str_putc(buf, '@'); + } + + git_str_puts(buf, url->host); + + if (url->port && !git_net_url_is_default_port(url)) { + git_str_putc(buf, ':'); + git_str_puts(buf, url->port); + } + + git_str_puts(buf, url->path ? url->path : "/"); + + if (url->query) { + git_str_putc(buf, '?'); + git_str_puts(buf, url->query); + } + + return git_str_oom(buf) ? -1 : 0; +} + +int git_net_url_fmt_path(git_str *buf, git_net_url *url) +{ + git_str_puts(buf, url->path ? url->path : "/"); + + if (url->query) { + git_str_putc(buf, '?'); + git_str_puts(buf, url->query); + } + + return git_str_oom(buf) ? -1 : 0; +} + +static bool matches_pattern( + git_net_url *url, + const char *pattern, + size_t pattern_len) +{ + const char *domain, *port = NULL, *colon; + size_t host_len, domain_len, port_len = 0, wildcard = 0; + + GIT_UNUSED(url); + GIT_UNUSED(pattern); + + if (!pattern_len) + return false; + else if (pattern_len == 1 && pattern[0] == '*') + return true; + else if (pattern_len > 1 && pattern[0] == '*' && pattern[1] == '.') + wildcard = 2; + else if (pattern[0] == '.') + wildcard = 1; + + domain = pattern + wildcard; + domain_len = pattern_len - wildcard; + + if ((colon = memchr(domain, ':', domain_len)) != NULL) { + domain_len = colon - domain; + port = colon + 1; + port_len = pattern_len - wildcard - domain_len - 1; + } + + /* A pattern's port *must* match if it's specified */ + if (port_len && git__strlcmp(url->port, port, port_len) != 0) + return false; + + /* No wildcard? Host must match exactly. */ + if (!wildcard) + return !git__strlcmp(url->host, domain, domain_len); + + /* Wildcard: ensure there's (at least) a suffix match */ + if ((host_len = strlen(url->host)) < domain_len || + memcmp(url->host + (host_len - domain_len), domain, domain_len)) + return false; + + /* The pattern is *.domain and the host is simply domain */ + if (host_len == domain_len) + return true; + + /* The pattern is *.domain and the host is foo.domain */ + return (url->host[host_len - domain_len - 1] == '.'); +} + +bool git_net_url_matches_pattern(git_net_url *url, const char *pattern) +{ + return matches_pattern(url, pattern, strlen(pattern)); +} + +bool git_net_url_matches_pattern_list( + git_net_url *url, + const char *pattern_list) +{ + const char *pattern, *pattern_end, *sep; + + for (pattern = pattern_list; + pattern && *pattern; + pattern = sep ? sep + 1 : NULL) { + sep = strchr(pattern, ','); + pattern_end = sep ? sep : strchr(pattern, '\0'); + + if (matches_pattern(url, pattern, (pattern_end - pattern))) + return true; + } + + return false; +} + +void git_net_url_dispose(git_net_url *url) +{ + if (url->username) + git__memzero(url->username, strlen(url->username)); + + if (url->password) + git__memzero(url->password, strlen(url->password)); + + git__free(url->scheme); url->scheme = NULL; + git__free(url->host); url->host = NULL; + git__free(url->port); url->port = NULL; + git__free(url->path); url->path = NULL; + git__free(url->query); url->query = NULL; + git__free(url->fragment); url->fragment = NULL; + git__free(url->username); url->username = NULL; + git__free(url->password); url->password = NULL; +} diff --git a/src/util/net.h b/src/util/net.h new file mode 100644 index 0000000..8024956 --- /dev/null +++ b/src/util/net.h @@ -0,0 +1,110 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_net_h__ +#define INCLUDE_net_h__ + +#include "git2_util.h" + +/* + * Hostname handling + */ + +/* + * See if a given hostname matches a certificate name pattern, according + * to RFC2818 rules (which specifies HTTP over TLS). Mainly, an asterisk + * matches anything, but is limited to a single url component. + */ +extern bool git_net_hostname_matches_cert( + const char *hostname, + const char *pattern); + +/* + * URL handling + */ + +typedef struct git_net_url { + char *scheme; + char *host; + char *port; + char *path; + char *query; + char *fragment; + char *username; + char *password; +} git_net_url; + +#define GIT_NET_URL_INIT { NULL } + +/** Is a given string a url? */ +extern bool git_net_str_is_url(const char *str); + +/** Duplicate a URL */ +extern int git_net_url_dup(git_net_url *out, git_net_url *in); + +/** Parses a string containing a URL into a structure. */ +extern int git_net_url_parse(git_net_url *url, const char *str); + +/** Parses a string containing an SCP style path into a URL structure. */ +extern int git_net_url_parse_scp(git_net_url *url, const char *str); + +/** + * Parses a string containing a standard URL or an SCP style path into + * a URL structure. + */ +extern int git_net_url_parse_standard_or_scp(git_net_url *url, const char *str); + +/** + * Parses a string containing an HTTP endpoint that may not be a + * well-formed URL. For example, "localhost" or "localhost:port". + */ +extern int git_net_url_parse_http( + git_net_url *url, + const char *str); + +/** Appends a path and/or query string to the given URL */ +extern int git_net_url_joinpath( + git_net_url *out, + git_net_url *in, + const char *path); + +/** Ensures that a URL is minimally valid (contains a host, port and path) */ +extern bool git_net_url_valid(git_net_url *url); + +/** Returns true if the URL is on the default port. */ +extern bool git_net_url_is_default_port(git_net_url *url); + +/** Returns true if the host portion of the URL is an ipv6 address. */ +extern bool git_net_url_is_ipv6(git_net_url *url); + +/* Applies a redirect to the URL with a git-aware service suffix. */ +extern int git_net_url_apply_redirect( + git_net_url *url, + const char *redirect_location, + bool allow_offsite, + const char *service_suffix); + +/** Swaps the contents of one URL for another. */ +extern void git_net_url_swap(git_net_url *a, git_net_url *b); + +/** Places the URL into the given buffer. */ +extern int git_net_url_fmt(git_str *out, git_net_url *url); + +/** Place the path and query string into the given buffer. */ +extern int git_net_url_fmt_path(git_str *buf, git_net_url *url); + +/** Determines if the url matches given pattern or pattern list */ +extern bool git_net_url_matches_pattern( + git_net_url *url, + const char *pattern); +extern bool git_net_url_matches_pattern_list( + git_net_url *url, + const char *pattern_list); + +/** Disposes the contents of the structure. */ +extern void git_net_url_dispose(git_net_url *url); + +#endif diff --git a/src/util/pool.c b/src/util/pool.c new file mode 100644 index 0000000..16ffa39 --- /dev/null +++ b/src/util/pool.c @@ -0,0 +1,260 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "pool.h" + +#include "posix.h" +#ifndef GIT_WIN32 +#include +#endif + +struct git_pool_page { + git_pool_page *next; + size_t size; + size_t avail; + GIT_ALIGN(char data[GIT_FLEX_ARRAY], 8); +}; + +static void *pool_alloc_page(git_pool *pool, size_t size); + +#ifndef GIT_DEBUG_POOL + +static size_t system_page_size = 0; + +int git_pool_global_init(void) +{ + if (git__page_size(&system_page_size) < 0) + system_page_size = 4096; + /* allow space for malloc overhead */ + system_page_size -= (2 * sizeof(void *)) + sizeof(git_pool_page); + return 0; +} + +int git_pool_init(git_pool *pool, size_t item_size) +{ + GIT_ASSERT_ARG(pool); + GIT_ASSERT_ARG(item_size >= 1); + + memset(pool, 0, sizeof(git_pool)); + pool->item_size = item_size; + pool->page_size = system_page_size; + + return 0; +} + +void git_pool_clear(git_pool *pool) +{ + git_pool_page *scan, *next; + + for (scan = pool->pages; scan != NULL; scan = next) { + next = scan->next; + git__free(scan); + } + + pool->pages = NULL; +} + +static void *pool_alloc_page(git_pool *pool, size_t size) +{ + git_pool_page *page; + const size_t new_page_size = (size <= pool->page_size) ? pool->page_size : size; + size_t alloc_size; + + if (GIT_ADD_SIZET_OVERFLOW(&alloc_size, new_page_size, sizeof(git_pool_page)) || + !(page = git__malloc(alloc_size))) + return NULL; + + page->size = new_page_size; + page->avail = new_page_size - size; + page->next = pool->pages; + + pool->pages = page; + + return page->data; +} + +static void *pool_alloc(git_pool *pool, size_t size) +{ + git_pool_page *page = pool->pages; + void *ptr = NULL; + + if (!page || page->avail < size) + return pool_alloc_page(pool, size); + + ptr = &page->data[page->size - page->avail]; + page->avail -= size; + + return ptr; +} + +uint32_t git_pool__open_pages(git_pool *pool) +{ + uint32_t ct = 0; + git_pool_page *scan; + for (scan = pool->pages; scan != NULL; scan = scan->next) ct++; + return ct; +} + +bool git_pool__ptr_in_pool(git_pool *pool, void *ptr) +{ + git_pool_page *scan; + for (scan = pool->pages; scan != NULL; scan = scan->next) + if ((void *)scan->data <= ptr && + (void *)(((char *)scan->data) + scan->size) > ptr) + return true; + return false; +} + +#else + +int git_pool_global_init(void) +{ + return 0; +} + +static int git_pool__ptr_cmp(const void * a, const void * b) +{ + if(a > b) { + return 1; + } + if(a < b) { + return -1; + } + else { + return 0; + } +} + +int git_pool_init(git_pool *pool, size_t item_size) +{ + GIT_ASSERT_ARG(pool); + GIT_ASSERT_ARG(item_size >= 1); + + memset(pool, 0, sizeof(git_pool)); + pool->item_size = item_size; + pool->page_size = git_pool__system_page_size(); + git_vector_init(&pool->allocations, 100, git_pool__ptr_cmp); + + return 0; +} + +void git_pool_clear(git_pool *pool) +{ + git_vector_free_deep(&pool->allocations); +} + +static void *pool_alloc(git_pool *pool, size_t size) { + void *ptr = NULL; + if((ptr = git__malloc(size)) == NULL) { + return NULL; + } + git_vector_insert_sorted(&pool->allocations, ptr, NULL); + return ptr; +} + +bool git_pool__ptr_in_pool(git_pool *pool, void *ptr) +{ + size_t pos; + return git_vector_bsearch(&pos, &pool->allocations, ptr) != GIT_ENOTFOUND; +} +#endif + +void git_pool_swap(git_pool *a, git_pool *b) +{ + git_pool temp; + + if (a == b) + return; + + memcpy(&temp, a, sizeof(temp)); + memcpy(a, b, sizeof(temp)); + memcpy(b, &temp, sizeof(temp)); +} + +static size_t alloc_size(git_pool *pool, size_t count) +{ + const size_t align = sizeof(void *) - 1; + + if (pool->item_size > 1) { + const size_t item_size = (pool->item_size + align) & ~align; + return item_size * count; + } + + return (count + align) & ~align; +} + +void *git_pool_malloc(git_pool *pool, size_t items) +{ + return pool_alloc(pool, alloc_size(pool, items)); +} + +void *git_pool_mallocz(git_pool *pool, size_t items) +{ + const size_t size = alloc_size(pool, items); + void *ptr = pool_alloc(pool, size); + if (ptr) + memset(ptr, 0x0, size); + return ptr; +} + +char *git_pool_strndup(git_pool *pool, const char *str, size_t n) +{ + char *ptr = NULL; + + GIT_ASSERT_ARG_WITH_RETVAL(pool, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(str, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(pool->item_size == sizeof(char), NULL); + + if (n == SIZE_MAX) + return NULL; + + if ((ptr = git_pool_malloc(pool, (n + 1))) != NULL) { + memcpy(ptr, str, n); + ptr[n] = '\0'; + } + + return ptr; +} + +char *git_pool_strdup(git_pool *pool, const char *str) +{ + GIT_ASSERT_ARG_WITH_RETVAL(pool, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(str, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(pool->item_size == sizeof(char), NULL); + + return git_pool_strndup(pool, str, strlen(str)); +} + +char *git_pool_strdup_safe(git_pool *pool, const char *str) +{ + return str ? git_pool_strdup(pool, str) : NULL; +} + +char *git_pool_strcat(git_pool *pool, const char *a, const char *b) +{ + void *ptr; + size_t len_a, len_b, total; + + GIT_ASSERT_ARG_WITH_RETVAL(pool, NULL); + GIT_ASSERT_ARG_WITH_RETVAL(pool->item_size == sizeof(char), NULL); + + len_a = a ? strlen(a) : 0; + len_b = b ? strlen(b) : 0; + + if (GIT_ADD_SIZET_OVERFLOW(&total, len_a, len_b) || + GIT_ADD_SIZET_OVERFLOW(&total, total, 1)) + return NULL; + + if ((ptr = git_pool_malloc(pool, total)) != NULL) { + if (len_a) + memcpy(ptr, a, len_a); + if (len_b) + memcpy(((char *)ptr) + len_a, b, len_b); + *(((char *)ptr) + len_a + len_b) = '\0'; + } + return ptr; +} diff --git a/src/util/pool.h b/src/util/pool.h new file mode 100644 index 0000000..0238431 --- /dev/null +++ b/src/util/pool.h @@ -0,0 +1,146 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_pool_h__ +#define INCLUDE_pool_h__ + +#include "git2_util.h" + +#include "vector.h" + +typedef struct git_pool_page git_pool_page; + +#ifndef GIT_DEBUG_POOL +/** + * Chunked allocator. + * + * A `git_pool` can be used when you want to cheaply allocate + * multiple items of the same type and are willing to free them + * all together with a single call. The two most common cases + * are a set of fixed size items (such as lots of OIDs) or a + * bunch of strings. + * + * Internally, a `git_pool` allocates pages of memory and then + * deals out blocks from the trailing unused portion of each page. + * The pages guarantee that the number of actual allocations done + * will be much smaller than the number of items needed. + * + * For examples of how to set up a `git_pool` see `git_pool_init`. + */ +typedef struct { + git_pool_page *pages; /* allocated pages */ + size_t item_size; /* size of single alloc unit in bytes */ + size_t page_size; /* size of page in bytes */ +} git_pool; + +#define GIT_POOL_INIT { NULL, 0, 0 } + +#else + +/** + * Debug chunked allocator. + * + * Acts just like `git_pool` but instead of actually pooling allocations it + * passes them through to `git__malloc`. This makes it possible to easily debug + * systems that use `git_pool` using valgrind. + * + * In order to track allocations during the lifetime of the pool we use a + * `git_vector`. When the pool is deallocated everything in the vector is + * freed. + * + * `API is exactly the same as the standard `git_pool` with one exception. + * Since we aren't allocating pages to hand out in chunks we can't easily + * implement `git_pool__open_pages`. + */ +typedef struct { + git_vector allocations; + size_t item_size; + size_t page_size; +} git_pool; + +#define GIT_POOL_INIT { GIT_VECTOR_INIT, 0, 0 } + +#endif + +/** + * Initialize a pool. + * + * To allocation strings, use like this: + * + * git_pool_init(&string_pool, 1); + * my_string = git_pool_strdup(&string_pool, your_string); + * + * To allocate items of fixed size, use like this: + * + * git_pool_init(&pool, sizeof(item)); + * my_item = git_pool_malloc(&pool, 1); + * + * Of course, you can use this in other ways, but those are the + * two most common patterns. + */ +extern int git_pool_init(git_pool *pool, size_t item_size); + +/** + * Free all items in pool + */ +extern void git_pool_clear(git_pool *pool); + +/** + * Swap two pools with one another + */ +extern void git_pool_swap(git_pool *a, git_pool *b); + +/** + * Allocate space for one or more items from a pool. + */ +extern void *git_pool_malloc(git_pool *pool, size_t items); +extern void *git_pool_mallocz(git_pool *pool, size_t items); + +/** + * Allocate space and duplicate string data into it. + * + * This is allowed only for pools with item_size == sizeof(char) + */ +extern char *git_pool_strndup(git_pool *pool, const char *str, size_t n); + +/** + * Allocate space and duplicate a string into it. + * + * This is allowed only for pools with item_size == sizeof(char) + */ +extern char *git_pool_strdup(git_pool *pool, const char *str); + +/** + * Allocate space and duplicate a string into it, NULL is no error. + * + * This is allowed only for pools with item_size == sizeof(char) + */ +extern char *git_pool_strdup_safe(git_pool *pool, const char *str); + +/** + * Allocate space for the concatenation of two strings. + * + * This is allowed only for pools with item_size == sizeof(char) + */ +extern char *git_pool_strcat(git_pool *pool, const char *a, const char *b); + +/* + * Misc utilities + */ +#ifndef GIT_DEBUG_POOL +extern uint32_t git_pool__open_pages(git_pool *pool); +#endif +extern bool git_pool__ptr_in_pool(git_pool *pool, void *ptr); + +/** + * This function is being called by our global setup routines to + * initialize the system pool size. + * + * @return 0 on success, <0 on failure + */ +extern int git_pool_global_init(void); + +#endif diff --git a/src/util/posix.c b/src/util/posix.c new file mode 100644 index 0000000..cfc0e07 --- /dev/null +++ b/src/util/posix.c @@ -0,0 +1,357 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "posix.h" + +#include "fs_path.h" +#include +#include + +size_t p_fsync__cnt = 0; + +#ifndef GIT_WIN32 + +#ifdef NO_ADDRINFO + +int p_getaddrinfo( + const char *host, + const char *port, + struct addrinfo *hints, + struct addrinfo **info) +{ + struct addrinfo *ainfo, *ai; + int p = 0; + + GIT_UNUSED(hints); + + if ((ainfo = git__malloc(sizeof(struct addrinfo))) == NULL) + return -1; + + if ((ainfo->ai_hostent = gethostbyname(host)) == NULL) { + git__free(ainfo); + return -2; + } + + ainfo->ai_servent = getservbyname(port, 0); + + if (ainfo->ai_servent) + ainfo->ai_port = ainfo->ai_servent->s_port; + else + ainfo->ai_port = htons(atol(port)); + + memcpy(&ainfo->ai_addr_in.sin_addr, + ainfo->ai_hostent->h_addr_list[0], + ainfo->ai_hostent->h_length); + + ainfo->ai_protocol = 0; + ainfo->ai_socktype = hints->ai_socktype; + ainfo->ai_family = ainfo->ai_hostent->h_addrtype; + ainfo->ai_addr_in.sin_family = ainfo->ai_family; + ainfo->ai_addr_in.sin_port = ainfo->ai_port; + ainfo->ai_addr = (struct addrinfo *)&ainfo->ai_addr_in; + ainfo->ai_addrlen = sizeof(struct sockaddr_in); + + *info = ainfo; + + if (ainfo->ai_hostent->h_addr_list[1] == NULL) { + ainfo->ai_next = NULL; + return 0; + } + + ai = ainfo; + + for (p = 1; ainfo->ai_hostent->h_addr_list[p] != NULL; p++) { + if (!(ai->ai_next = git__malloc(sizeof(struct addrinfo)))) { + p_freeaddrinfo(ainfo); + return -1; + } + memcpy(ai->ai_next, ainfo, sizeof(struct addrinfo)); + memcpy(&ai->ai_next->ai_addr_in.sin_addr, + ainfo->ai_hostent->h_addr_list[p], + ainfo->ai_hostent->h_length); + ai->ai_next->ai_addr = (struct addrinfo *)&ai->ai_next->ai_addr_in; + ai = ai->ai_next; + } + + ai->ai_next = NULL; + return 0; +} + +void p_freeaddrinfo(struct addrinfo *info) +{ + struct addrinfo *p, *next; + + p = info; + + while(p != NULL) { + next = p->ai_next; + git__free(p); + p = next; + } +} + +const char *p_gai_strerror(int ret) +{ + switch(ret) { + case -1: return "Out of memory"; break; + case -2: return "Address lookup failed"; break; + default: return "Unknown error"; break; + } +} + +#endif /* NO_ADDRINFO */ + +int p_open(const char *path, volatile int flags, ...) +{ + mode_t mode = 0; + + #ifdef GIT_DEBUG_STRICT_OPEN + if (strstr(path, "//") != NULL) { + errno = EACCES; + return -1; + } + #endif + + if (flags & O_CREAT) { + va_list arg_list; + + va_start(arg_list, flags); + mode = (mode_t)va_arg(arg_list, int); + va_end(arg_list); + } + + return open(path, flags | O_BINARY | O_CLOEXEC, mode); +} + +int p_creat(const char *path, mode_t mode) +{ + return open(path, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | O_CLOEXEC, mode); +} + +int p_getcwd(char *buffer_out, size_t size) +{ + char *cwd_buffer; + + GIT_ASSERT_ARG(buffer_out); + GIT_ASSERT_ARG(size > 0); + + cwd_buffer = getcwd(buffer_out, size); + + if (cwd_buffer == NULL) + return -1; + + git_fs_path_mkposix(buffer_out); + git_fs_path_string_to_dir(buffer_out, size); /* append trailing slash */ + + return 0; +} + +int p_rename(const char *from, const char *to) +{ + if (!link(from, to)) { + p_unlink(from); + return 0; + } + + if (!rename(from, to)) + return 0; + + return -1; +} + +#endif /* GIT_WIN32 */ + +ssize_t p_read(git_file fd, void *buf, size_t cnt) +{ + char *b = buf; + + if (!git__is_ssizet(cnt)) { +#ifdef GIT_WIN32 + SetLastError(ERROR_INVALID_PARAMETER); +#endif + errno = EINVAL; + return -1; + } + + while (cnt) { + ssize_t r; +#ifdef GIT_WIN32 + r = read(fd, b, cnt > INT_MAX ? INT_MAX : (unsigned int)cnt); +#else + r = read(fd, b, cnt); +#endif + if (r < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + return -1; + } + if (!r) + break; + cnt -= r; + b += r; + } + return (b - (char *)buf); +} + +int p_write(git_file fd, const void *buf, size_t cnt) +{ + const char *b = buf; + + while (cnt) { + ssize_t r; +#ifdef GIT_WIN32 + GIT_ASSERT((size_t)((unsigned int)cnt) == cnt); + r = write(fd, b, (unsigned int)cnt); +#else + r = write(fd, b, cnt); +#endif + if (r < 0) { + if (errno == EINTR || GIT_ISBLOCKED(errno)) + continue; + return -1; + } + if (!r) { + errno = EPIPE; + return -1; + } + cnt -= r; + b += r; + } + return 0; +} + +#ifdef NO_MMAP + +#include "map.h" + +int git__page_size(size_t *page_size) +{ + /* dummy; here we don't need any alignment anyway */ + *page_size = 4096; + return 0; +} + +int git__mmap_alignment(size_t *alignment) +{ + /* dummy; here we don't need any alignment anyway */ + *alignment = 4096; + return 0; +} + + +int p_mmap(git_map *out, size_t len, int prot, int flags, int fd, off64_t offset) +{ + const char *ptr; + size_t remaining_len; + + GIT_MMAP_VALIDATE(out, len, prot, flags); + + /* writes cannot be emulated without handling pagefaults since write happens by + * writing to mapped memory */ + if (prot & GIT_PROT_WRITE) { + git_error_set(GIT_ERROR_OS, "trying to map %s-writeable", + ((flags & GIT_MAP_TYPE) == GIT_MAP_SHARED) ? "shared": "private"); + return -1; + } + + if (!git__is_ssizet(len)) { + errno = EINVAL; + return -1; + } + + out->len = 0; + out->data = git__malloc(len); + GIT_ERROR_CHECK_ALLOC(out->data); + + remaining_len = len; + ptr = (const char *)out->data; + while (remaining_len > 0) { + ssize_t nb; + HANDLE_EINTR(nb, p_pread(fd, (void *)ptr, remaining_len, offset)); + if (nb <= 0) { + git_error_set(GIT_ERROR_OS, "mmap emulation failed"); + git__free(out->data); + out->data = NULL; + return -1; + } + + ptr += nb; + offset += nb; + remaining_len -= nb; + } + + out->len = len; + return 0; +} + +int p_munmap(git_map *map) +{ + GIT_ASSERT_ARG(map); + git__free(map->data); + + /* Initializing will help debug use-after-free */ + map->len = 0; + map->data = NULL; + + return 0; +} + +#endif + +#if defined(GIT_IO_POLL) || defined(GIT_IO_WSAPOLL) + +/* Handled by posix.h; this test simplifies the final else */ + +#elif defined(GIT_IO_SELECT) + +int p_poll(struct pollfd *fds, unsigned int nfds, int timeout_ms) +{ + fd_set read_fds, write_fds, except_fds; + struct timeval timeout = { 0, 0 }; + unsigned int i; + int max_fd = -1, ret; + + FD_ZERO(&read_fds); + FD_ZERO(&write_fds); + FD_ZERO(&except_fds); + + for (i = 0; i < nfds; i++) { + if ((fds[i].events & POLLIN)) + FD_SET(fds[i].fd, &read_fds); + + if ((fds[i].events & POLLOUT)) + FD_SET(fds[i].fd, &write_fds); + + if ((fds[i].events & POLLPRI)) + FD_SET(fds[i].fd, &except_fds); + + max_fd = MAX(max_fd, fds[i].fd); + } + + if (timeout_ms > 0) { + timeout.tv_sec = timeout_ms / 1000; + timeout.tv_usec = (timeout_ms % 1000) * 1000; + } + + if ((ret = select(max_fd + 1, &read_fds, &write_fds, &except_fds, + timeout_ms < 0 ? NULL : &timeout)) < 0) + goto done; + + for (i = 0; i < nfds; i++) { + fds[i].revents = 0 | + FD_ISSET(fds[i].fd, &read_fds) ? POLLIN : 0 | + FD_ISSET(fds[i].fd, &write_fds) ? POLLOUT : 0 | + FD_ISSET(fds[i].fd, &except_fds) ? POLLPRI : 0; + } + +done: + return ret; +} + +#else +# error no poll compatible implementation +#endif diff --git a/src/util/posix.h b/src/util/posix.h new file mode 100644 index 0000000..7470745 --- /dev/null +++ b/src/util/posix.h @@ -0,0 +1,220 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_posix_h__ +#define INCLUDE_posix_h__ + +#include "git2_util.h" + +#include +#include +#include + +/* stat: file mode type testing macros */ +#ifndef S_IFGITLINK +#define S_IFGITLINK 0160000 +#define S_ISGITLINK(m) (((m) & S_IFMT) == S_IFGITLINK) +#endif + +#ifndef S_IFLNK +#define S_IFLNK 0120000 +#undef _S_IFLNK +#define _S_IFLNK S_IFLNK +#endif + +#ifndef S_IWUSR +#define S_IWUSR 00200 +#endif + +#ifndef S_IXUSR +#define S_IXUSR 00100 +#endif + +#ifndef S_ISLNK +#define S_ISLNK(m) (((m) & _S_IFMT) == _S_IFLNK) +#endif + +#ifndef S_ISDIR +#define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR) +#endif + +#ifndef S_ISREG +#define S_ISREG(m) (((m) & _S_IFMT) == _S_IFREG) +#endif + +#ifndef S_ISFIFO +#define S_ISFIFO(m) (((m) & _S_IFMT) == _S_IFIFO) +#endif + +/* if S_ISGID is not defined, then don't try to set it */ +#ifndef S_ISGID +#define S_ISGID 0 +#endif + +#ifndef O_BINARY +#define O_BINARY 0 +#endif +#ifndef O_CLOEXEC +#define O_CLOEXEC 0 +#endif +#ifndef SOCK_CLOEXEC +#define SOCK_CLOEXEC 0 +#endif + +/* access() mode parameter #defines */ +#ifndef F_OK +#define F_OK 0 /* existence check */ +#endif +#ifndef W_OK +#define W_OK 2 /* write mode check */ +#endif +#ifndef R_OK +#define R_OK 4 /* read mode check */ +#endif + +/* Determine whether an errno value indicates that a read or write failed + * because the descriptor is blocked. + */ +#if defined(EWOULDBLOCK) +#define GIT_ISBLOCKED(e) ((e) == EAGAIN || (e) == EWOULDBLOCK) +#else +#define GIT_ISBLOCKED(e) ((e) == EAGAIN) +#endif + +/* define some standard errnos that the runtime may be missing. for example, + * mingw lacks EAFNOSUPPORT. */ +#ifndef EAFNOSUPPORT +#define EAFNOSUPPORT (INT_MAX-1) +#endif + +/* Compiler independent macro to handle signal interrpted system calls */ +#define HANDLE_EINTR(result, x) do { \ + result = (x); \ + } while (result == -1 && errno == EINTR); + + +/* Provide a 64-bit size for offsets. */ + +#if defined(_MSC_VER) +typedef __int64 off64_t; +#elif defined(__HAIKU__) +typedef __haiku_std_int64 off64_t; +#elif defined(__APPLE__) +typedef __int64_t off64_t; +#elif defined(_AIX) +typedef long long off64_t; +#else +typedef int64_t off64_t; +#endif + +typedef int git_file; + +/** + * Standard POSIX Methods + * + * All the methods starting with the `p_` prefix are + * direct ports of the standard POSIX methods. + * + * Some of the methods are slightly wrapped to provide + * saner defaults. Some of these methods are emulated + * in Windows platforms. + * + * Use your manpages to check the docs on these. + */ + +extern ssize_t p_read(git_file fd, void *buf, size_t cnt); +extern int p_write(git_file fd, const void *buf, size_t cnt); + +extern ssize_t p_pread(int fd, void *data, size_t size, off64_t offset); +extern ssize_t p_pwrite(int fd, const void *data, size_t size, off64_t offset); + +#define p_close(fd) close(fd) +#define p_umask(m) umask(m) + +extern int p_open(const char *path, int flags, ...); +extern int p_creat(const char *path, mode_t mode); +extern int p_getcwd(char *buffer_out, size_t size); +extern int p_rename(const char *from, const char *to); + +extern int git__page_size(size_t *page_size); +extern int git__mmap_alignment(size_t *page_size); + +/* The number of times `p_fsync` has been called. Note that this is for + * test code only; it it not necessarily thread-safe and should not be + * relied upon in production. + */ +extern size_t p_fsync__cnt; + +/** + * Platform-dependent methods + */ +#ifdef GIT_WIN32 +# include "win32/posix.h" +#else +# include "unix/posix.h" +#endif + +#include "strnlen.h" + +#ifdef NO_READDIR_R +GIT_INLINE(int) p_readdir_r(DIR *dirp, struct dirent *entry, struct dirent **result) +{ + GIT_UNUSED(entry); + *result = readdir(dirp); + return 0; +} +#else /* NO_READDIR_R */ +# define p_readdir_r(d,e,r) readdir_r(d,e,r) +#endif + +#ifdef NO_ADDRINFO +# include +struct addrinfo { + struct hostent *ai_hostent; + struct servent *ai_servent; + struct sockaddr_in ai_addr_in; + struct sockaddr *ai_addr; + size_t ai_addrlen; + int ai_family; + int ai_socktype; + int ai_protocol; + long ai_port; + struct addrinfo *ai_next; +}; + +extern int p_getaddrinfo(const char *host, const char *port, + struct addrinfo *hints, struct addrinfo **info); +extern void p_freeaddrinfo(struct addrinfo *info); +extern const char *p_gai_strerror(int ret); +#else +# define p_getaddrinfo(a, b, c, d) getaddrinfo(a, b, c, d) +# define p_freeaddrinfo(a) freeaddrinfo(a) +# define p_gai_strerror(c) gai_strerror(c) +#endif /* NO_ADDRINFO */ + +#ifdef GIT_IO_POLL +# include +# define p_poll poll +#elif GIT_IO_WSAPOLL +# include +# define p_poll WSAPoll +#else +# define POLLIN 0x01 +# define POLLPRI 0x02 +# define POLLOUT 0x04 +# define POLLERR 0x08 +# define POLLHUP 0x10 + +struct pollfd { + int fd; + short events; + short revents; +}; + +extern int p_poll(struct pollfd *fds, unsigned int nfds, int timeout); +#endif + +#endif diff --git a/src/util/pqueue.c b/src/util/pqueue.c new file mode 100644 index 0000000..3820e99 --- /dev/null +++ b/src/util/pqueue.c @@ -0,0 +1,125 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "pqueue.h" + +#include "util.h" + +#define PQUEUE_LCHILD_OF(I) (((I)<<1)+1) +#define PQUEUE_RCHILD_OF(I) (((I)<<1)+2) +#define PQUEUE_PARENT_OF(I) (((I)-1)>>1) + +int git_pqueue_init( + git_pqueue *pq, + uint32_t flags, + size_t init_size, + git_vector_cmp cmp) +{ + int error = git_vector_init(pq, init_size, cmp); + + if (!error) { + /* mix in our flags */ + pq->flags |= flags; + + /* if fixed size heap, pretend vector is exactly init_size elements */ + if ((flags & GIT_PQUEUE_FIXED_SIZE) && init_size > 0) + pq->_alloc_size = init_size; + } + + return error; +} + +static void pqueue_up(git_pqueue *pq, size_t el) +{ + size_t parent_el = PQUEUE_PARENT_OF(el); + void *kid = git_vector_get(pq, el); + + while (el > 0) { + void *parent = pq->contents[parent_el]; + + if (pq->_cmp(parent, kid) <= 0) + break; + + pq->contents[el] = parent; + + el = parent_el; + parent_el = PQUEUE_PARENT_OF(el); + } + + pq->contents[el] = kid; +} + +static void pqueue_down(git_pqueue *pq, size_t el) +{ + void *parent = git_vector_get(pq, el), *kid, *rkid; + + while (1) { + size_t kid_el = PQUEUE_LCHILD_OF(el); + + if ((kid = git_vector_get(pq, kid_el)) == NULL) + break; + + if ((rkid = git_vector_get(pq, kid_el + 1)) != NULL && + pq->_cmp(kid, rkid) > 0) { + kid = rkid; + kid_el += 1; + } + + if (pq->_cmp(parent, kid) <= 0) + break; + + pq->contents[el] = kid; + el = kid_el; + } + + pq->contents[el] = parent; +} + +int git_pqueue_insert(git_pqueue *pq, void *item) +{ + int error = 0; + + /* if heap is full, pop the top element if new one should replace it */ + if ((pq->flags & GIT_PQUEUE_FIXED_SIZE) != 0 && + pq->length >= pq->_alloc_size) + { + /* skip this item if below min item in heap or if + * we do not have a comparison function */ + if (!pq->_cmp || pq->_cmp(item, git_vector_get(pq, 0)) <= 0) + return 0; + /* otherwise remove the min item before inserting new */ + (void)git_pqueue_pop(pq); + } + + if (!(error = git_vector_insert(pq, item)) && pq->_cmp) + pqueue_up(pq, pq->length - 1); + + return error; +} + +void *git_pqueue_pop(git_pqueue *pq) +{ + void *rval; + + if (!pq->_cmp) { + rval = git_vector_last(pq); + } else { + rval = git_pqueue_get(pq, 0); + } + + if (git_pqueue_size(pq) > 1 && pq->_cmp) { + /* move last item to top of heap, shrink, and push item down */ + pq->contents[0] = git_vector_last(pq); + git_vector_pop(pq); + pqueue_down(pq, 0); + } else { + /* all we need to do is shrink the heap in this case */ + git_vector_pop(pq); + } + + return rval; +} diff --git a/src/util/pqueue.h b/src/util/pqueue.h new file mode 100644 index 0000000..97232b4 --- /dev/null +++ b/src/util/pqueue.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_pqueue_h__ +#define INCLUDE_pqueue_h__ + +#include "git2_util.h" + +#include "vector.h" + +typedef git_vector git_pqueue; + +enum { + /* flag meaning: don't grow heap, keep highest values only */ + GIT_PQUEUE_FIXED_SIZE = (GIT_VECTOR_FLAG_MAX << 1) +}; + +/** + * Initialize priority queue + * + * @param pq The priority queue struct to initialize + * @param flags Flags (see above) to control queue behavior + * @param init_size The initial queue size + * @param cmp The entry priority comparison function + * @return 0 on success, <0 on error + */ +extern int git_pqueue_init( + git_pqueue *pq, + uint32_t flags, + size_t init_size, + git_vector_cmp cmp); + +#define git_pqueue_free git_vector_free +#define git_pqueue_clear git_vector_clear +#define git_pqueue_size git_vector_length +#define git_pqueue_get git_vector_get +#define git_pqueue_reverse git_vector_reverse + +/** + * Insert a new item into the queue + * + * @param pq The priority queue + * @param item Pointer to the item data + * @return 0 on success, <0 on failure + */ +extern int git_pqueue_insert(git_pqueue *pq, void *item); + +/** + * Remove the top item in the priority queue + * + * @param pq The priority queue + * @return item from heap on success, NULL if queue is empty + */ +extern void *git_pqueue_pop(git_pqueue *pq); + +#endif diff --git a/src/util/rand.c b/src/util/rand.c new file mode 100644 index 0000000..2ed0605 --- /dev/null +++ b/src/util/rand.c @@ -0,0 +1,236 @@ +/* Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) + +To the extent possible under law, the author has dedicated all copyright +and related and neighboring rights to this software to the public domain +worldwide. This software is distributed without any warranty. + +See . */ + +#include "git2_util.h" +#include "rand.h" +#include "runtime.h" + +#if defined(GIT_RAND_GETENTROPY) +# include +#endif + +#if defined(GIT_WIN32) +# include +#endif + +static uint64_t state[4]; +static git_mutex state_lock; + +typedef union { + double f; + uint64_t d; +} bits; + +#if defined(GIT_WIN32) +GIT_INLINE(int) getseed(uint64_t *seed) +{ + HCRYPTPROV provider; + SYSTEMTIME systemtime; + FILETIME filetime, idletime, kerneltime, usertime; + + if (CryptAcquireContext(&provider, 0, 0, PROV_RSA_FULL, + CRYPT_VERIFYCONTEXT|CRYPT_SILENT)) { + BOOL success = CryptGenRandom(provider, sizeof(uint64_t), (void *)seed); + CryptReleaseContext(provider, 0); + + if (success) + return 0; + } + + GetSystemTime(&systemtime); + if (!SystemTimeToFileTime(&systemtime, &filetime)) { + git_error_set(GIT_ERROR_OS, "could not get time for random seed"); + return -1; + } + + /* Fall-through: generate a seed from the time and system state */ + *seed = 0; + *seed |= ((uint64_t)filetime.dwLowDateTime << 32); + *seed |= ((uint64_t)filetime.dwHighDateTime); + + GetSystemTimes(&idletime, &kerneltime, &usertime); + + *seed ^= ((uint64_t)idletime.dwLowDateTime << 32); + *seed ^= ((uint64_t)kerneltime.dwLowDateTime); + *seed ^= ((uint64_t)usertime.dwLowDateTime << 32); + + *seed ^= ((uint64_t)idletime.dwHighDateTime); + *seed ^= ((uint64_t)kerneltime.dwHighDateTime << 12); + *seed ^= ((uint64_t)usertime.dwHighDateTime << 24); + + *seed ^= ((uint64_t)GetCurrentProcessId() << 32); + *seed ^= ((uint64_t)GetCurrentThreadId() << 48); + + *seed ^= git_time_monotonic(); + + /* Mix in the addresses of some functions and variables */ + *seed ^= (((uint64_t)((uintptr_t)seed) << 32)); + *seed ^= (((uint64_t)((uintptr_t)&errno))); + + return 0; +} + +#else + +GIT_INLINE(int) getseed(uint64_t *seed) +{ + struct timeval tv; + double loadavg[3]; + int fd; + +# if defined(GIT_RAND_GETLOADAVG) + bits convert; +# endif + +# if defined(GIT_RAND_GETENTROPY) + GIT_UNUSED((fd = 0)); + + if (getentropy(seed, sizeof(uint64_t)) == 0) + return 0; +# else + /* + * Try to read from /dev/urandom; most modern systems will have + * this, but we may be chrooted, etc, so it's not a fatal error + */ + if ((fd = open("/dev/urandom", O_RDONLY)) >= 0) { + ssize_t ret = read(fd, seed, sizeof(uint64_t)); + close(fd); + + if (ret == (ssize_t)sizeof(uint64_t)) + return 0; + } +# endif + + /* Fall-through: generate a seed from the time and system state */ + if (gettimeofday(&tv, NULL) < 0) { + git_error_set(GIT_ERROR_OS, "could get time for random seed"); + return -1; + } + + *seed = 0; + *seed |= ((uint64_t)tv.tv_usec << 40); + *seed |= ((uint64_t)tv.tv_sec); + + *seed ^= ((uint64_t)getpid() << 48); + *seed ^= ((uint64_t)getppid() << 32); + *seed ^= ((uint64_t)getpgid(0) << 28); + *seed ^= ((uint64_t)getsid(0) << 16); + *seed ^= ((uint64_t)getuid() << 8); + *seed ^= ((uint64_t)getgid()); + +# if defined(GIT_RAND_GETLOADAVG) + getloadavg(loadavg, 3); + + convert.f = loadavg[0]; *seed ^= (convert.d >> 36); + convert.f = loadavg[1]; *seed ^= (convert.d); + convert.f = loadavg[2]; *seed ^= (convert.d >> 16); +# else + GIT_UNUSED(loadavg[0]); +# endif + + *seed ^= git_time_monotonic(); + + /* Mix in the addresses of some variables */ + *seed ^= ((uint64_t)((size_t)((void *)seed)) << 32); + *seed ^= ((uint64_t)((size_t)((void *)&errno))); + + return 0; +} +#endif + +static void git_rand_global_shutdown(void) +{ + git_mutex_free(&state_lock); +} + +int git_rand_global_init(void) +{ + uint64_t seed = 0; + + if (git_mutex_init(&state_lock) < 0 || getseed(&seed) < 0) + return -1; + + if (!seed) { + git_error_set(GIT_ERROR_INTERNAL, "failed to generate random seed"); + return -1; + } + + git_rand_seed(seed); + git_runtime_shutdown_register(git_rand_global_shutdown); + + return 0; +} + +/* + * This is splitmix64. xoroshiro256** uses 256 bit seed; this is used + * to generate 256 bits of seed from the given 64, per the author's + * recommendation. + */ +GIT_INLINE(uint64_t) splitmix64(uint64_t *in) +{ + uint64_t z; + + *in += 0x9e3779b97f4a7c15; + + z = *in; + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; + z = (z ^ (z >> 27)) * 0x94d049bb133111eb; + return z ^ (z >> 31); +} + +void git_rand_seed(uint64_t seed) +{ + uint64_t mixer; + + mixer = seed; + + git_mutex_lock(&state_lock); + state[0] = splitmix64(&mixer); + state[1] = splitmix64(&mixer); + state[2] = splitmix64(&mixer); + state[3] = splitmix64(&mixer); + git_mutex_unlock(&state_lock); +} + +/* This is xoshiro256** 1.0, one of our all-purpose, rock-solid + generators. It has excellent (sub-ns) speed, a state (256 bits) that is + large enough for any parallel application, and it passes all tests we + are aware of. + + For generating just floating-point numbers, xoshiro256+ is even faster. + + The state must be seeded so that it is not everywhere zero. If you have + a 64-bit seed, we suggest to seed a splitmix64 generator and use its + output to fill s. */ + +GIT_INLINE(uint64_t) rotl(const uint64_t x, int k) { + return (x << k) | (x >> (64 - k)); +} + +uint64_t git_rand_next(void) { + uint64_t t, result; + + git_mutex_lock(&state_lock); + + result = rotl(state[1] * 5, 7) * 9; + + t = state[1] << 17; + + state[2] ^= state[0]; + state[3] ^= state[1]; + state[1] ^= state[2]; + state[0] ^= state[3]; + + state[2] ^= t; + + state[3] = rotl(state[3], 45); + + git_mutex_unlock(&state_lock); + + return result; +} diff --git a/src/util/rand.h b/src/util/rand.h new file mode 100644 index 0000000..fa0619a --- /dev/null +++ b/src/util/rand.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_rand_h__ +#define INCLUDE_rand_h__ + +#include "git2_util.h" + +/** + * Initialize the random number generation subsystem. This will + * seed the random number generator with the system's entropy pool, + * if available, and will fall back to the current time and + * system information if not. + */ +int git_rand_global_init(void); + +/** + * Seed the pseudo-random number generator. This is not needed to be + * called; the PRNG is seeded by `git_rand_global_init`, but it may + * be useful for testing. When the same seed is specified, the same + * sequence of random numbers from `git_rand_next` is emitted. + * + * @param seed the seed to use + */ +void git_rand_seed(uint64_t seed); + +/** + * Get the next pseudo-random number in the sequence. + * + * @return a 64-bit pseudo-random number + */ +uint64_t git_rand_next(void); + +#endif diff --git a/src/util/regexp.c b/src/util/regexp.c new file mode 100644 index 0000000..0870088 --- /dev/null +++ b/src/util/regexp.c @@ -0,0 +1,221 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "regexp.h" + +#if defined(GIT_REGEX_BUILTIN) || defined(GIT_REGEX_PCRE) + +int git_regexp_compile(git_regexp *r, const char *pattern, int flags) +{ + int erroffset, cflags = 0; + const char *error = NULL; + + if (flags & GIT_REGEXP_ICASE) + cflags |= PCRE_CASELESS; + + if ((*r = pcre_compile(pattern, cflags, &error, &erroffset, NULL)) == NULL) { + git_error_set_str(GIT_ERROR_REGEX, error); + return GIT_EINVALIDSPEC; + } + + return 0; +} + +void git_regexp_dispose(git_regexp *r) +{ + pcre_free(*r); + *r = NULL; +} + +int git_regexp_match(const git_regexp *r, const char *string) +{ + int error; + if ((error = pcre_exec(*r, NULL, string, (int) strlen(string), 0, 0, NULL, 0)) < 0) + return (error == PCRE_ERROR_NOMATCH) ? GIT_ENOTFOUND : GIT_EINVALIDSPEC; + return 0; +} + +int git_regexp_search(const git_regexp *r, const char *string, size_t nmatches, git_regmatch *matches) +{ + int static_ovec[9] = {0}, *ovec; + int error; + size_t i; + + /* The ovec array always needs to be a multiple of three */ + if (nmatches <= ARRAY_SIZE(static_ovec) / 3) + ovec = static_ovec; + else + ovec = git__calloc(nmatches * 3, sizeof(*ovec)); + GIT_ERROR_CHECK_ALLOC(ovec); + + if ((error = pcre_exec(*r, NULL, string, (int) strlen(string), 0, 0, ovec, (int) nmatches * 3)) < 0) + goto out; + + if (error == 0) + error = (int) nmatches; + + for (i = 0; i < (unsigned int) error; i++) { + matches[i].start = (ovec[i * 2] < 0) ? -1 : ovec[i * 2]; + matches[i].end = (ovec[i * 2 + 1] < 0) ? -1 : ovec[i * 2 + 1]; + } + for (i = (unsigned int) error; i < nmatches; i++) + matches[i].start = matches[i].end = -1; + +out: + if (nmatches > ARRAY_SIZE(static_ovec) / 3) + git__free(ovec); + if (error < 0) + return (error == PCRE_ERROR_NOMATCH) ? GIT_ENOTFOUND : GIT_EINVALIDSPEC; + return 0; +} + +#elif defined(GIT_REGEX_PCRE2) + +int git_regexp_compile(git_regexp *r, const char *pattern, int flags) +{ + unsigned char errmsg[1024]; + PCRE2_SIZE erroff; + int error, cflags = 0; + + if (flags & GIT_REGEXP_ICASE) + cflags |= PCRE2_CASELESS; + + if ((*r = pcre2_compile((const unsigned char *) pattern, PCRE2_ZERO_TERMINATED, + cflags, &error, &erroff, NULL)) == NULL) { + pcre2_get_error_message(error, errmsg, sizeof(errmsg)); + git_error_set_str(GIT_ERROR_REGEX, (char *) errmsg); + return GIT_EINVALIDSPEC; + } + + return 0; +} + +void git_regexp_dispose(git_regexp *r) +{ + pcre2_code_free(*r); + *r = NULL; +} + +int git_regexp_match(const git_regexp *r, const char *string) +{ + pcre2_match_data *data; + int error; + + data = pcre2_match_data_create(1, NULL); + GIT_ERROR_CHECK_ALLOC(data); + + error = pcre2_match(*r, (const unsigned char *) string, strlen(string), 0, 0, data, NULL); + pcre2_match_data_free(data); + if (error < 0) + return (error == PCRE2_ERROR_NOMATCH) ? GIT_ENOTFOUND : GIT_EINVALIDSPEC; + + return 0; +} + +int git_regexp_search(const git_regexp *r, const char *string, size_t nmatches, git_regmatch *matches) +{ + pcre2_match_data *data = NULL; + PCRE2_SIZE *ovec; + int error; + size_t i; + + if ((data = pcre2_match_data_create(nmatches, NULL)) == NULL) { + git_error_set_oom(); + goto out; + } + + if ((error = pcre2_match(*r, (const unsigned char *) string, strlen(string), + 0, 0, data, NULL)) < 0) + goto out; + + if (error == 0 || (unsigned int) error > nmatches) + error = nmatches; + ovec = pcre2_get_ovector_pointer(data); + + for (i = 0; i < (unsigned int) error; i++) { + matches[i].start = (ovec[i * 2] == PCRE2_UNSET) ? -1 : (ssize_t) ovec[i * 2]; + matches[i].end = (ovec[i * 2 + 1] == PCRE2_UNSET) ? -1 : (ssize_t) ovec[i * 2 + 1]; + } + for (i = (unsigned int) error; i < nmatches; i++) + matches[i].start = matches[i].end = -1; + +out: + pcre2_match_data_free(data); + if (error < 0) + return (error == PCRE2_ERROR_NOMATCH) ? GIT_ENOTFOUND : GIT_EINVALIDSPEC; + return 0; +} + +#elif defined(GIT_REGEX_REGCOMP) || defined(GIT_REGEX_REGCOMP_L) + +#if defined(GIT_REGEX_REGCOMP_L) +# include +#endif + +int git_regexp_compile(git_regexp *r, const char *pattern, int flags) +{ + int cflags = REG_EXTENDED, error; + char errmsg[1024]; + + if (flags & GIT_REGEXP_ICASE) + cflags |= REG_ICASE; + +# if defined(GIT_REGEX_REGCOMP) + if ((error = regcomp(r, pattern, cflags)) != 0) +# else + if ((error = regcomp_l(r, pattern, cflags, (locale_t) 0)) != 0) +# endif + { + regerror(error, r, errmsg, sizeof(errmsg)); + git_error_set_str(GIT_ERROR_REGEX, errmsg); + return GIT_EINVALIDSPEC; + } + + return 0; +} + +void git_regexp_dispose(git_regexp *r) +{ + regfree(r); +} + +int git_regexp_match(const git_regexp *r, const char *string) +{ + int error; + if ((error = regexec(r, string, 0, NULL, 0)) != 0) + return (error == REG_NOMATCH) ? GIT_ENOTFOUND : GIT_EINVALIDSPEC; + return 0; +} + +int git_regexp_search(const git_regexp *r, const char *string, size_t nmatches, git_regmatch *matches) +{ + regmatch_t static_m[3], *m; + int error; + size_t i; + + if (nmatches <= ARRAY_SIZE(static_m)) + m = static_m; + else + m = git__calloc(nmatches, sizeof(*m)); + + if ((error = regexec(r, string, nmatches, m, 0)) != 0) + goto out; + + for (i = 0; i < nmatches; i++) { + matches[i].start = (m[i].rm_so < 0) ? -1 : m[i].rm_so; + matches[i].end = (m[i].rm_eo < 0) ? -1 : m[i].rm_eo; + } + +out: + if (nmatches > ARRAY_SIZE(static_m)) + git__free(m); + if (error) + return (error == REG_NOMATCH) ? GIT_ENOTFOUND : GIT_EINVALIDSPEC; + return 0; +} + +#endif diff --git a/src/util/regexp.h b/src/util/regexp.h new file mode 100644 index 0000000..d0862b1 --- /dev/null +++ b/src/util/regexp.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_regexp_h__ +#define INCLUDE_regexp_h__ + +#include "git2_util.h" + +#if defined(GIT_REGEX_BUILTIN) || defined(GIT_REGEX_PCRE) +# include "pcre.h" +typedef pcre *git_regexp; +# define GIT_REGEX_INIT NULL +#elif defined(GIT_REGEX_PCRE2) +# define PCRE2_CODE_UNIT_WIDTH 8 +# include +typedef pcre2_code *git_regexp; +# define GIT_REGEX_INIT NULL +#elif defined(GIT_REGEX_REGCOMP) || defined(GIT_REGEX_REGCOMP_L) +# include +typedef regex_t git_regexp; +# define GIT_REGEX_INIT { 0 } +#else +# error "No regex backend" +#endif + +/** Options supported by @git_regexp_compile. */ +typedef enum { + /** Enable case-insensitive matching */ + GIT_REGEXP_ICASE = (1 << 0) +} git_regexp_flags_t; + +/** Structure containing information about regular expression matching groups */ +typedef struct { + /** Start of the given match. -1 if the group didn't match anything */ + ssize_t start; + /** End of the given match. -1 if the group didn't match anything */ + ssize_t end; +} git_regmatch; + +/** + * Compile a regular expression. The compiled expression needs to + * be cleaned up afterwards with `git_regexp_dispose`. + * + * @param r Pointer to the storage where to initialize the regular expression. + * @param pattern The pattern that shall be compiled. + * @param flags Flags to alter how the pattern shall be handled. + * 0 for defaults, otherwise see @git_regexp_flags_t. + * @return 0 on success, otherwise a negative return value. + */ +int git_regexp_compile(git_regexp *r, const char *pattern, int flags); + +/** + * Free memory associated with the regular expression + * + * @param r The regular expression structure to dispose. + */ +void git_regexp_dispose(git_regexp *r); + +/** + * Test whether a given string matches a compiled regular + * expression. + * + * @param r Compiled regular expression. + * @param string String to match against the regular expression. + * @return 0 if the string matches, a negative error code + * otherwise. GIT_ENOTFOUND if no match was found, + * GIT_EINVALIDSPEC if the regular expression matching + * was invalid. + */ +int git_regexp_match(const git_regexp *r, const char *string); + +/** + * Search for matches inside of a given string. + * + * Given a regular expression with capturing groups, this + * function will populate provided @git_regmatch structures with + * offsets for each of the given matches. Non-matching groups + * will have start and end values of the respective @git_regmatch + * structure set to -1. + * + * @param r Compiled regular expression. + * @param string String to match against the regular expression. + * @param nmatches Number of @git_regmatch structures provided by + * the user. + * @param matches Pointer to an array of @git_regmatch structures. + * @return 0 if the string matches, a negative error code + * otherwise. GIT_ENOTFOUND if no match was found, + * GIT_EINVALIDSPEC if the regular expression matching + * was invalid. + */ +int git_regexp_search(const git_regexp *r, const char *string, size_t nmatches, git_regmatch *matches); + +#endif diff --git a/src/util/runtime.c b/src/util/runtime.c new file mode 100644 index 0000000..a7711ff --- /dev/null +++ b/src/util/runtime.c @@ -0,0 +1,162 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2_util.h" +#include "runtime.h" + +static git_runtime_shutdown_fn shutdown_callback[32]; +static git_atomic32 shutdown_callback_count; + +static git_atomic32 init_count; + +static int init_common(git_runtime_init_fn init_fns[], size_t cnt) +{ + size_t i; + int ret; + + /* Initialize subsystems that have global state */ + for (i = 0; i < cnt; i++) { + if ((ret = init_fns[i]()) != 0) + break; + } + + GIT_MEMORY_BARRIER; + + return ret; +} + +static void shutdown_common(void) +{ + git_runtime_shutdown_fn cb; + int pos; + + for (pos = git_atomic32_get(&shutdown_callback_count); + pos > 0; + pos = git_atomic32_dec(&shutdown_callback_count)) { + cb = git_atomic_swap(shutdown_callback[pos - 1], NULL); + + if (cb != NULL) + cb(); + } +} + +int git_runtime_shutdown_register(git_runtime_shutdown_fn callback) +{ + int count = git_atomic32_inc(&shutdown_callback_count); + + if (count > (int)ARRAY_SIZE(shutdown_callback) || count == 0) { + git_error_set(GIT_ERROR_INVALID, + "too many shutdown callbacks registered"); + git_atomic32_dec(&shutdown_callback_count); + return -1; + } + + shutdown_callback[count - 1] = callback; + + return 0; +} + +#if defined(GIT_THREADS) && defined(GIT_WIN32) + +/* + * On Win32, we use a spinlock to provide locking semantics. This is + * lighter-weight than a proper critical section. + */ +static volatile LONG init_spinlock = 0; + +GIT_INLINE(int) init_lock(void) +{ + while (InterlockedCompareExchange(&init_spinlock, 1, 0)) { Sleep(0); } + return 0; +} + +GIT_INLINE(int) init_unlock(void) +{ + InterlockedExchange(&init_spinlock, 0); + return 0; +} + +#elif defined(GIT_THREADS) && defined(_POSIX_THREADS) + +/* + * On POSIX, we need to use a proper mutex for locking. We might prefer + * a spinlock here, too, but there's no static initializer for a + * pthread_spinlock_t. + */ +static pthread_mutex_t init_mutex = PTHREAD_MUTEX_INITIALIZER; + +GIT_INLINE(int) init_lock(void) +{ + return pthread_mutex_lock(&init_mutex) == 0 ? 0 : -1; +} + +GIT_INLINE(int) init_unlock(void) +{ + return pthread_mutex_unlock(&init_mutex) == 0 ? 0 : -1; +} + +#elif defined(GIT_THREADS) +# error unknown threading model +#else + +# define init_lock() git__noop() +# define init_unlock() git__noop() + +#endif + +int git_runtime_init(git_runtime_init_fn init_fns[], size_t cnt) +{ + int ret; + + if (init_lock() < 0) + return -1; + + /* Only do work on a 0 -> 1 transition of the refcount */ + if ((ret = git_atomic32_inc(&init_count)) == 1) { + if (init_common(init_fns, cnt) < 0) + ret = -1; + } + + if (init_unlock() < 0) + return -1; + + return ret; +} + +int git_runtime_init_count(void) +{ + int ret; + + if (init_lock() < 0) + return -1; + + ret = git_atomic32_get(&init_count); + + if (init_unlock() < 0) + return -1; + + return ret; +} + +int git_runtime_shutdown(void) +{ + int ret; + + /* Enter the lock */ + if (init_lock() < 0) + return -1; + + /* Only do work on a 1 -> 0 transition of the refcount */ + if ((ret = git_atomic32_dec(&init_count)) == 0) + shutdown_common(); + + /* Exit the lock */ + if (init_unlock() < 0) + return -1; + + return ret; +} diff --git a/src/util/runtime.h b/src/util/runtime.h new file mode 100644 index 0000000..6cbfd60 --- /dev/null +++ b/src/util/runtime.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_runtime_h__ +#define INCLUDE_runtime_h__ + +#include "git2_util.h" + +typedef int (*git_runtime_init_fn)(void); +typedef void (*git_runtime_shutdown_fn)(void); + +/** + * Start up a new runtime. If this is the first time that this + * function is called within the context of the current library + * or executable, then the given `init_fns` will be invoked. If + * it is not the first time, they will be ignored. + * + * The given initialization functions _may_ register shutdown + * handlers using `git_runtime_shutdown_register` to be notified + * when the runtime is shutdown. + * + * @param init_fns The list of initialization functions to call + * @param cnt The number of init_fns + * @return The number of initializations performed (including this one) or an error + */ +int git_runtime_init(git_runtime_init_fn init_fns[], size_t cnt); + +/* + * Returns the number of initializations active (the number of calls to + * `git_runtime_init` minus the number of calls sto `git_runtime_shutdown`). + * If 0, the runtime is not currently initialized. + * + * @return The number of initializations performed or an error + */ +int git_runtime_init_count(void); + +/** + * Shut down the runtime. If this is the last shutdown call, + * such that there are no remaining `init` calls, then any + * shutdown hooks that have been registered will be invoked. + * + * The number of outstanding initializations will be returned. + * If this number is 0, then the runtime is shutdown. + * + * @return The number of outstanding initializations (after this one) or an error + */ +int git_runtime_shutdown(void); + +/** + * Register a shutdown handler for this runtime. This should be done + * by a function invoked by `git_runtime_init` to ensure that the + * appropriate locks are taken. + * + * @param callback The shutdown handler callback + * @return 0 or an error code + */ +int git_runtime_shutdown_register(git_runtime_shutdown_fn callback); + +#endif diff --git a/src/util/sortedcache.c b/src/util/sortedcache.c new file mode 100644 index 0000000..7ff900e --- /dev/null +++ b/src/util/sortedcache.c @@ -0,0 +1,380 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "sortedcache.h" + +int git_sortedcache_new( + git_sortedcache **out, + size_t item_path_offset, + git_sortedcache_free_item_fn free_item, + void *free_item_payload, + git_vector_cmp item_cmp, + const char *path) +{ + git_sortedcache *sc; + size_t pathlen, alloclen; + + pathlen = path ? strlen(path) : 0; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, sizeof(git_sortedcache), pathlen); + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, 1); + sc = git__calloc(1, alloclen); + GIT_ERROR_CHECK_ALLOC(sc); + + if (git_pool_init(&sc->pool, 1) < 0 || + git_vector_init(&sc->items, 4, item_cmp) < 0 || + git_strmap_new(&sc->map) < 0) + goto fail; + + if (git_rwlock_init(&sc->lock)) { + git_error_set(GIT_ERROR_OS, "failed to initialize lock"); + goto fail; + } + + sc->item_path_offset = item_path_offset; + sc->free_item = free_item; + sc->free_item_payload = free_item_payload; + GIT_REFCOUNT_INC(sc); + if (pathlen) + memcpy(sc->path, path, pathlen); + + *out = sc; + return 0; + +fail: + git_strmap_free(sc->map); + git_vector_free(&sc->items); + git_pool_clear(&sc->pool); + git__free(sc); + return -1; +} + +void git_sortedcache_incref(git_sortedcache *sc) +{ + GIT_REFCOUNT_INC(sc); +} + +const char *git_sortedcache_path(git_sortedcache *sc) +{ + return sc->path; +} + +static void sortedcache_clear(git_sortedcache *sc) +{ + git_strmap_clear(sc->map); + + if (sc->free_item) { + size_t i; + void *item; + + git_vector_foreach(&sc->items, i, item) { + sc->free_item(sc->free_item_payload, item); + } + } + + git_vector_clear(&sc->items); + + git_pool_clear(&sc->pool); +} + +static void sortedcache_free(git_sortedcache *sc) +{ + /* acquire write lock to make sure everyone else is done */ + if (git_sortedcache_wlock(sc) < 0) + return; + + sortedcache_clear(sc); + git_vector_free(&sc->items); + git_strmap_free(sc->map); + + git_sortedcache_wunlock(sc); + + git_rwlock_free(&sc->lock); + git__free(sc); +} + +void git_sortedcache_free(git_sortedcache *sc) +{ + if (!sc) + return; + GIT_REFCOUNT_DEC(sc, sortedcache_free); +} + +static int sortedcache_copy_item(void *payload, void *tgt_item, void *src_item) +{ + git_sortedcache *sc = payload; + /* path will already have been copied by upsert */ + memcpy(tgt_item, src_item, sc->item_path_offset); + return 0; +} + +/* copy a sorted cache */ +int git_sortedcache_copy( + git_sortedcache **out, + git_sortedcache *src, + bool lock, + int (*copy_item)(void *payload, void *tgt_item, void *src_item), + void *payload) +{ + int error = 0; + git_sortedcache *tgt; + size_t i; + void *src_item, *tgt_item; + + /* just use memcpy if no special copy fn is passed in */ + if (!copy_item) { + copy_item = sortedcache_copy_item; + payload = src; + } + + if ((error = git_sortedcache_new( + &tgt, src->item_path_offset, + src->free_item, src->free_item_payload, + src->items._cmp, src->path)) < 0) + return error; + + if (lock && git_sortedcache_rlock(src) < 0) { + git_sortedcache_free(tgt); + return -1; + } + + git_vector_foreach(&src->items, i, src_item) { + char *path = ((char *)src_item) + src->item_path_offset; + + if ((error = git_sortedcache_upsert(&tgt_item, tgt, path)) < 0 || + (error = copy_item(payload, tgt_item, src_item)) < 0) + break; + } + + if (lock) + git_sortedcache_runlock(src); + if (error) + git_sortedcache_free(tgt); + + *out = !error ? tgt : NULL; + + return error; +} + +/* lock sortedcache while making modifications */ +int git_sortedcache_wlock(git_sortedcache *sc) +{ + GIT_UNUSED(sc); /* prevent warning when compiled w/o threads */ + + if (git_rwlock_wrlock(&sc->lock) < 0) { + git_error_set(GIT_ERROR_OS, "unable to acquire write lock on cache"); + return -1; + } + return 0; +} + +/* unlock sorted cache when done with modifications */ +void git_sortedcache_wunlock(git_sortedcache *sc) +{ + git_vector_sort(&sc->items); + git_rwlock_wrunlock(&sc->lock); +} + +/* lock sortedcache for read */ +int git_sortedcache_rlock(git_sortedcache *sc) +{ + GIT_UNUSED(sc); /* prevent warning when compiled w/o threads */ + + if (git_rwlock_rdlock(&sc->lock) < 0) { + git_error_set(GIT_ERROR_OS, "unable to acquire read lock on cache"); + return -1; + } + return 0; +} + +/* unlock sorted cache when done reading */ +void git_sortedcache_runlock(git_sortedcache *sc) +{ + GIT_UNUSED(sc); /* prevent warning when compiled w/o threads */ + git_rwlock_rdunlock(&sc->lock); +} + +/* if the file has changed, lock cache and load file contents into buf; + * returns <0 on error, >0 if file has not changed + */ +int git_sortedcache_lockandload(git_sortedcache *sc, git_str *buf) +{ + int error, fd; + struct stat st; + + if ((error = git_sortedcache_wlock(sc)) < 0) + return error; + + if ((error = git_futils_filestamp_check(&sc->stamp, sc->path)) <= 0) + goto unlock; + + if ((fd = git_futils_open_ro(sc->path)) < 0) { + error = fd; + goto unlock; + } + + if (p_fstat(fd, &st) < 0) { + git_error_set(GIT_ERROR_OS, "failed to stat file"); + error = -1; + (void)p_close(fd); + goto unlock; + } + + if (!git__is_sizet(st.st_size)) { + git_error_set(GIT_ERROR_INVALID, "unable to load file larger than size_t"); + error = -1; + (void)p_close(fd); + goto unlock; + } + + if (buf) + error = git_futils_readbuffer_fd(buf, fd, (size_t)st.st_size); + + (void)p_close(fd); + + if (error < 0) + goto unlock; + + return 1; /* return 1 -> file needs reload and was successfully loaded */ + +unlock: + git_sortedcache_wunlock(sc); + return error; +} + +void git_sortedcache_updated(git_sortedcache *sc) +{ + /* update filestamp to latest value */ + git_futils_filestamp_check(&sc->stamp, sc->path); +} + +/* release all items in sorted cache */ +int git_sortedcache_clear(git_sortedcache *sc, bool wlock) +{ + if (wlock && git_sortedcache_wlock(sc) < 0) + return -1; + + sortedcache_clear(sc); + + if (wlock) + git_sortedcache_wunlock(sc); + + return 0; +} + +/* find and/or insert item, returning pointer to item data */ +int git_sortedcache_upsert(void **out, git_sortedcache *sc, const char *key) +{ + size_t keylen, itemlen; + int error = 0; + char *item_key; + void *item; + + if ((item = git_strmap_get(sc->map, key)) != NULL) + goto done; + + keylen = strlen(key); + itemlen = sc->item_path_offset + keylen + 1; + itemlen = (itemlen + 7) & ~7; + + if ((item = git_pool_mallocz(&sc->pool, itemlen)) == NULL) { + /* don't use GIT_ERROR_CHECK_ALLOC b/c of lock */ + error = -1; + goto done; + } + + /* one strange thing is that even if the vector or hash table insert + * fail, there is no way to free the pool item so we just abandon it + */ + + item_key = ((char *)item) + sc->item_path_offset; + memcpy(item_key, key, keylen); + + if ((error = git_strmap_set(sc->map, item_key, item)) < 0) + goto done; + + if ((error = git_vector_insert(&sc->items, item)) < 0) + git_strmap_delete(sc->map, item_key); + +done: + if (out) + *out = !error ? item : NULL; + return error; +} + +/* lookup item by key */ +void *git_sortedcache_lookup(const git_sortedcache *sc, const char *key) +{ + return git_strmap_get(sc->map, key); +} + +/* find out how many items are in the cache */ +size_t git_sortedcache_entrycount(const git_sortedcache *sc) +{ + return git_vector_length(&sc->items); +} + +/* lookup item by index */ +void *git_sortedcache_entry(git_sortedcache *sc, size_t pos) +{ + /* make sure the items are sorted so this gets the correct item */ + if (!git_vector_is_sorted(&sc->items)) + git_vector_sort(&sc->items); + + return git_vector_get(&sc->items, pos); +} + +/* helper struct so bsearch callback can know offset + key value for cmp */ +struct sortedcache_magic_key { + size_t offset; + const char *key; +}; + +static int sortedcache_magic_cmp(const void *key, const void *value) +{ + const struct sortedcache_magic_key *magic = key; + const char *value_key = ((const char *)value) + magic->offset; + return strcmp(magic->key, value_key); +} + +/* lookup index of item by key */ +int git_sortedcache_lookup_index( + size_t *out, git_sortedcache *sc, const char *key) +{ + struct sortedcache_magic_key magic; + + magic.offset = sc->item_path_offset; + magic.key = key; + + return git_vector_bsearch2(out, &sc->items, sortedcache_magic_cmp, &magic); +} + +/* remove entry from cache */ +int git_sortedcache_remove(git_sortedcache *sc, size_t pos) +{ + char *item; + + /* + * Because of pool allocation, this can't actually remove the item, + * but we can remove it from the items vector and the hash table. + */ + + if ((item = git_vector_get(&sc->items, pos)) == NULL) { + git_error_set(GIT_ERROR_INVALID, "removing item out of range"); + return GIT_ENOTFOUND; + } + + (void)git_vector_remove(&sc->items, pos); + + git_strmap_delete(sc->map, item + sc->item_path_offset); + + if (sc->free_item) + sc->free_item(sc->free_item_payload, item); + + return 0; +} + diff --git a/src/util/sortedcache.h b/src/util/sortedcache.h new file mode 100644 index 0000000..3eee465 --- /dev/null +++ b/src/util/sortedcache.h @@ -0,0 +1,182 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_sorted_cache_h__ +#define INCLUDE_sorted_cache_h__ + +#include "git2_util.h" + +#include "util.h" +#include "futils.h" +#include "vector.h" +#include "thread.h" +#include "pool.h" +#include "strmap.h" + +#include + +/* + * The purpose of this data structure is to cache the parsed contents of a + * file (a.k.a. the backing file) where each item in the file can be + * identified by a key string and you want to both look them up by name + * and traverse them in sorted order. Each item is assumed to itself end + * in a GIT_FLEX_ARRAY. + */ + +typedef void (*git_sortedcache_free_item_fn)(void *payload, void *item); + +typedef struct { + git_refcount rc; + git_rwlock lock; + size_t item_path_offset; + git_sortedcache_free_item_fn free_item; + void *free_item_payload; + git_pool pool; + git_vector items; + git_strmap *map; + git_futils_filestamp stamp; + char path[GIT_FLEX_ARRAY]; +} git_sortedcache; + +/* Create a new sortedcache + * + * Even though every sortedcache stores items with a GIT_FLEX_ARRAY at + * the end containing their key string, you have to provide the item_cmp + * sorting function because the sorting function doesn't get a payload + * and therefore can't know the offset to the item key string. :-( + * + * @param out The allocated git_sortedcache + * @param item_path_offset Offset to the GIT_FLEX_ARRAY item key in the + * struct - use offsetof(struct mine, key-field) to get this + * @param free_item Optional callback to free each item + * @param free_item_payload Optional payload passed to free_item callback + * @param item_cmp Compare the keys of two items + * @param path The path to the backing store file for this cache; this + * may be NULL. The cache makes it easy to load this and check + * if it has been modified since the last load and/or write. + */ +GIT_WARN_UNUSED_RESULT int git_sortedcache_new( + git_sortedcache **out, + size_t item_path_offset, /* use offsetof(struct, path-field) macro */ + git_sortedcache_free_item_fn free_item, + void *free_item_payload, + git_vector_cmp item_cmp, + const char *path); + +/* Copy a sorted cache + * + * - `copy_item` can be NULL to just use memcpy + * - if `lock`, grabs read lock on `src` during copy and releases after + */ +GIT_WARN_UNUSED_RESULT int git_sortedcache_copy( + git_sortedcache **out, + git_sortedcache *src, + bool lock, + int (*copy_item)(void *payload, void *tgt_item, void *src_item), + void *payload); + +/* Free sorted cache (first calling `free_item` callbacks) + * + * Don't call on a locked collection - it may acquire a write lock + */ +void git_sortedcache_free(git_sortedcache *sc); + +/* Increment reference count - balance with call to free */ +void git_sortedcache_incref(git_sortedcache *sc); + +/* Get the pathname associated with this cache at creation time */ +const char *git_sortedcache_path(git_sortedcache *sc); + +/* + * CACHE WRITE FUNCTIONS + * + * The following functions require you to have a writer lock to make the + * modification. Some of the functions take a `wlock` parameter and + * will optionally lock and unlock for you if that is passed as true. + * + */ + +/* Lock sortedcache for write */ +GIT_WARN_UNUSED_RESULT int git_sortedcache_wlock(git_sortedcache *sc); + +/* Unlock sorted cache when done with write */ +void git_sortedcache_wunlock(git_sortedcache *sc); + +/* Lock cache and load backing file into a buffer. + * + * This grabs a write lock on the cache then looks at the modification + * time and size of the file on disk. + * + * If the file appears to have changed, this loads the file contents into + * the buffer and returns a positive value leaving the cache locked - the + * caller should parse the file content, update the cache as needed, then + * release the lock. NOTE: In this case, the caller MUST unlock the cache. + * + * If the file appears to be unchanged, then this automatically releases + * the lock on the cache, clears the buffer, and returns 0. + * + * @return 0 if up-to-date, 1 if out-of-date, <0 on error + */ +GIT_WARN_UNUSED_RESULT int git_sortedcache_lockandload( + git_sortedcache *sc, git_str *buf); + +/* Refresh file timestamp after write completes + * You should already be holding the write lock when you call this. + */ +void git_sortedcache_updated(git_sortedcache *sc); + +/* Release all items in sorted cache + * + * If `wlock` is true, grabs write lock and releases when done, otherwise + * you should already be holding a write lock when you call this. + */ +GIT_WARN_UNUSED_RESULT int git_sortedcache_clear( + git_sortedcache *sc, bool wlock); + +/* Find and/or insert item, returning pointer to item data. + * You should already be holding the write lock when you call this. + */ +GIT_WARN_UNUSED_RESULT int git_sortedcache_upsert( + void **out, git_sortedcache *sc, const char *key); + +/* Removes entry at pos from cache + * You should already be holding the write lock when you call this. + */ +int git_sortedcache_remove(git_sortedcache *sc, size_t pos); + +/* + * CACHE READ FUNCTIONS + * + * The following functions access items in the cache. To prevent the + * results from being invalidated before they can be used, you should be + * holding either a read lock or a write lock when using these functions. + * + */ + +/* Lock sortedcache for read */ +GIT_WARN_UNUSED_RESULT int git_sortedcache_rlock(git_sortedcache *sc); + +/* Unlock sorted cache when done with read */ +void git_sortedcache_runlock(git_sortedcache *sc); + +/* Lookup item by key - returns NULL if not found */ +void *git_sortedcache_lookup(const git_sortedcache *sc, const char *key); + +/* Get how many items are in the cache + * + * You can call this function without holding a lock, but be aware + * that it may change before you use it. + */ +size_t git_sortedcache_entrycount(const git_sortedcache *sc); + +/* Lookup item by index - returns NULL if out of range */ +void *git_sortedcache_entry(git_sortedcache *sc, size_t pos); + +/* Lookup index of item by key - returns GIT_ENOTFOUND if not found */ +int git_sortedcache_lookup_index( + size_t *out, git_sortedcache *sc, const char *key); + +#endif diff --git a/src/util/staticstr.h b/src/util/staticstr.h new file mode 100644 index 0000000..b7d0790 --- /dev/null +++ b/src/util/staticstr.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_stackstr_h__ +#define INCLUDE_stackstr_h__ + +#include "git2_util.h" + +typedef struct { + /* Length of / number of bytes used by `data`. */ + size_t len; + + /* Size of the allocated `data` buffer. */ + size_t size; + + /* The actual string buffer data. */ + char data[GIT_FLEX_ARRAY]; +} git_staticstr; + +#define git_staticstr_with_size(__size) \ + struct { \ + size_t len; \ + size_t size; \ + char data[__size]; \ + } + +#define git_staticstr_init(__str, __size) \ + do { \ + (__str)->len = 0; \ + (__str)->size = __size; \ + (__str)->data[0] = '\0'; \ + } while(0) + +#define git_staticstr_offset(__str) \ + ((__str)->data + (__str)->len) + +#define git_staticstr_remain(__str) \ + ((__str)->len > (__str)->size ? 0 : ((__str)->size - (__str)->len)) + +#define git_staticstr_increase(__str, __len) \ + do { ((__str)->len += __len); } while(0) + +#define git_staticstr_consume_bytes(__str, __len) \ + do { git_staticstr_consume(__str, (__str)->data + __len); } while(0) + +#define git_staticstr_consume(__str, __end) \ + do { \ + if (__end > (__str)->data && \ + __end <= (__str)->data + (__str)->len) { \ + size_t __consumed = __end - (__str)->data; \ + memmove((__str)->data, __end, (__str)->len - __consumed); \ + (__str)->len -= __consumed; \ + (__str)->data[(__str)->len] = '\0'; \ + } \ + } while(0) + +#define git_staticstr_clear(__str) \ + do { \ + (__str)->len = 0; \ + (__str)->data[0] = 0; \ + } while(0) + +#endif diff --git a/src/util/str.c b/src/util/str.c new file mode 100644 index 0000000..0d405bf --- /dev/null +++ b/src/util/str.c @@ -0,0 +1,1372 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "str.h" +#include "posix.h" +#include + +/* Used as default value for git_str->ptr so that people can always + * assume ptr is non-NULL and zero terminated even for new git_strs. + */ +char git_str__initstr[1]; + +char git_str__oom[1]; + +#define ENSURE_SIZE(b, d) \ + if ((b)->ptr == git_str__oom || \ + ((d) > (b)->asize && git_str_grow((b), (d)) < 0))\ + return -1; + + +int git_str_init(git_str *buf, size_t initial_size) +{ + buf->asize = 0; + buf->size = 0; + buf->ptr = git_str__initstr; + + ENSURE_SIZE(buf, initial_size); + + return 0; +} + +int git_str_try_grow( + git_str *buf, size_t target_size, bool mark_oom) +{ + char *new_ptr; + size_t new_size; + + if (buf->ptr == git_str__oom) + return -1; + + if (buf->asize == 0 && buf->size != 0) { + git_error_set(GIT_ERROR_INVALID, "cannot grow a borrowed buffer"); + return GIT_EINVALID; + } + + if (!target_size) + target_size = buf->size; + + if (target_size <= buf->asize) + return 0; + + if (buf->asize == 0) { + new_size = target_size; + new_ptr = NULL; + } else { + new_size = buf->asize; + /* + * Grow the allocated buffer by 1.5 to allow + * re-use of memory holes resulting from the + * realloc. If this is still too small, then just + * use the target size. + */ + if ((new_size = (new_size << 1) - (new_size >> 1)) < target_size) + new_size = target_size; + new_ptr = buf->ptr; + } + + /* round allocation up to multiple of 8 */ + new_size = (new_size + 7) & ~7; + + if (new_size < buf->size) { + if (mark_oom) { + if (buf->ptr && buf->ptr != git_str__initstr) + git__free(buf->ptr); + buf->ptr = git_str__oom; + } + + git_error_set_oom(); + return -1; + } + + new_ptr = git__realloc(new_ptr, new_size); + + if (!new_ptr) { + if (mark_oom) { + if (buf->ptr && (buf->ptr != git_str__initstr)) + git__free(buf->ptr); + buf->ptr = git_str__oom; + } + return -1; + } + + buf->asize = new_size; + buf->ptr = new_ptr; + + /* truncate the existing buffer size if necessary */ + if (buf->size >= buf->asize) + buf->size = buf->asize - 1; + buf->ptr[buf->size] = '\0'; + + return 0; +} + +int git_str_grow(git_str *buffer, size_t target_size) +{ + return git_str_try_grow(buffer, target_size, true); +} + +int git_str_grow_by(git_str *buffer, size_t additional_size) +{ + size_t newsize; + + if (GIT_ADD_SIZET_OVERFLOW(&newsize, buffer->size, additional_size)) { + buffer->ptr = git_str__oom; + return -1; + } + + return git_str_try_grow(buffer, newsize, true); +} + +void git_str_dispose(git_str *buf) +{ + if (!buf) return; + + if (buf->asize > 0 && buf->ptr != NULL && buf->ptr != git_str__oom) + git__free(buf->ptr); + + git_str_init(buf, 0); +} + +void git_str_clear(git_str *buf) +{ + buf->size = 0; + + if (!buf->ptr) { + buf->ptr = git_str__initstr; + buf->asize = 0; + } + + if (buf->asize > 0) + buf->ptr[0] = '\0'; +} + +int git_str_set(git_str *buf, const void *data, size_t len) +{ + size_t alloclen; + + if (len == 0 || data == NULL) { + git_str_clear(buf); + } else { + if (data != buf->ptr) { + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, len, 1); + ENSURE_SIZE(buf, alloclen); + memmove(buf->ptr, data, len); + } + + buf->size = len; + if (buf->asize > buf->size) + buf->ptr[buf->size] = '\0'; + + } + return 0; +} + +int git_str_sets(git_str *buf, const char *string) +{ + return git_str_set(buf, string, string ? strlen(string) : 0); +} + +int git_str_putc(git_str *buf, char c) +{ + size_t new_size; + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, buf->size, 2); + ENSURE_SIZE(buf, new_size); + buf->ptr[buf->size++] = c; + buf->ptr[buf->size] = '\0'; + return 0; +} + +int git_str_putcn(git_str *buf, char c, size_t len) +{ + size_t new_size; + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, buf->size, len); + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, new_size, 1); + ENSURE_SIZE(buf, new_size); + memset(buf->ptr + buf->size, c, len); + buf->size += len; + buf->ptr[buf->size] = '\0'; + return 0; +} + +int git_str_put(git_str *buf, const char *data, size_t len) +{ + if (len) { + size_t new_size; + + GIT_ASSERT_ARG(data); + + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, buf->size, len); + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, new_size, 1); + ENSURE_SIZE(buf, new_size); + memmove(buf->ptr + buf->size, data, len); + buf->size += len; + buf->ptr[buf->size] = '\0'; + } + return 0; +} + +int git_str_puts(git_str *buf, const char *string) +{ + GIT_ASSERT_ARG(string); + + return git_str_put(buf, string, strlen(string)); +} + +static char hex_encode[] = "0123456789abcdef"; + +int git_str_encode_hexstr(git_str *str, const char *data, size_t len) +{ + size_t new_size, i; + char *s; + + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&new_size, len, 2); + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, new_size, 1); + + if (git_str_grow_by(str, new_size) < 0) + return -1; + + s = str->ptr + str->size; + + for (i = 0; i < len; i++) { + *s++ = hex_encode[(data[i] & 0xf0) >> 4]; + *s++ = hex_encode[(data[i] & 0x0f)]; + } + + str->size += (len * 2); + str->ptr[str->size] = '\0'; + + return 0; +} + +static const char base64_encode[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +int git_str_encode_base64(git_str *buf, const char *data, size_t len) +{ + size_t extra = len % 3; + uint8_t *write, a, b, c; + const uint8_t *read = (const uint8_t *)data; + size_t blocks = (len / 3) + !!extra, alloclen; + + GIT_ERROR_CHECK_ALLOC_ADD(&blocks, blocks, 1); + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&alloclen, blocks, 4); + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, buf->size); + + ENSURE_SIZE(buf, alloclen); + write = (uint8_t *)&buf->ptr[buf->size]; + + /* convert each run of 3 bytes into 4 output bytes */ + for (len -= extra; len > 0; len -= 3) { + a = *read++; + b = *read++; + c = *read++; + + *write++ = base64_encode[a >> 2]; + *write++ = base64_encode[(a & 0x03) << 4 | b >> 4]; + *write++ = base64_encode[(b & 0x0f) << 2 | c >> 6]; + *write++ = base64_encode[c & 0x3f]; + } + + if (extra > 0) { + a = *read++; + b = (extra > 1) ? *read++ : 0; + + *write++ = base64_encode[a >> 2]; + *write++ = base64_encode[(a & 0x03) << 4 | b >> 4]; + *write++ = (extra > 1) ? base64_encode[(b & 0x0f) << 2] : '='; + *write++ = '='; + } + + buf->size = ((char *)write) - buf->ptr; + buf->ptr[buf->size] = '\0'; + + return 0; +} + +/* The inverse of base64_encode */ +static const int8_t base64_decode[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 0, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 +}; + +int git_str_decode_base64(git_str *buf, const char *base64, size_t len) +{ + size_t i; + int8_t a, b, c, d; + size_t orig_size = buf->size, new_size; + + if (len % 4) { + git_error_set(GIT_ERROR_INVALID, "invalid base64 input"); + return -1; + } + + GIT_ASSERT_ARG(len % 4 == 0); + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, (len / 4 * 3), buf->size); + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, new_size, 1); + ENSURE_SIZE(buf, new_size); + + for (i = 0; i < len; i += 4) { + if ((a = base64_decode[(unsigned char)base64[i]]) < 0 || + (b = base64_decode[(unsigned char)base64[i+1]]) < 0 || + (c = base64_decode[(unsigned char)base64[i+2]]) < 0 || + (d = base64_decode[(unsigned char)base64[i+3]]) < 0) { + buf->size = orig_size; + buf->ptr[buf->size] = '\0'; + + git_error_set(GIT_ERROR_INVALID, "invalid base64 input"); + return -1; + } + + buf->ptr[buf->size++] = ((a << 2) | (b & 0x30) >> 4); + buf->ptr[buf->size++] = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2); + buf->ptr[buf->size++] = (c & 0x03) << 6 | (d & 0x3f); + } + + buf->ptr[buf->size] = '\0'; + return 0; +} + +static const char base85_encode[] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"; + +int git_str_encode_base85(git_str *buf, const char *data, size_t len) +{ + size_t blocks = (len / 4) + !!(len % 4), alloclen; + + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&alloclen, blocks, 5); + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, buf->size); + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, 1); + + ENSURE_SIZE(buf, alloclen); + + while (len) { + uint32_t acc = 0; + char b85[5]; + int i; + + for (i = 24; i >= 0; i -= 8) { + uint8_t ch = *data++; + acc |= (uint32_t)ch << i; + + if (--len == 0) + break; + } + + for (i = 4; i >= 0; i--) { + int val = acc % 85; + acc /= 85; + + b85[i] = base85_encode[val]; + } + + for (i = 0; i < 5; i++) + buf->ptr[buf->size++] = b85[i]; + } + + buf->ptr[buf->size] = '\0'; + + return 0; +} + +/* The inverse of base85_encode */ +static const int8_t base85_decode[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 63, -1, 64, 65, 66, 67, -1, 68, 69, 70, 71, -1, 72, -1, -1, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -1, 73, 74, 75, 76, 77, + 78, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, -1, -1, -1, 79, 80, + 81, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 82, 83, 84, 85, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 +}; + +int git_str_decode_base85( + git_str *buf, + const char *base85, + size_t base85_len, + size_t output_len) +{ + size_t orig_size = buf->size, new_size; + + if (base85_len % 5 || + output_len > base85_len * 4 / 5) { + git_error_set(GIT_ERROR_INVALID, "invalid base85 input"); + return -1; + } + + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, output_len, buf->size); + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, new_size, 1); + ENSURE_SIZE(buf, new_size); + + while (output_len) { + unsigned acc = 0; + int de, cnt = 4; + unsigned char ch; + do { + ch = *base85++; + de = base85_decode[ch]; + if (--de < 0) + goto on_error; + + acc = acc * 85 + de; + } while (--cnt); + ch = *base85++; + de = base85_decode[ch]; + if (--de < 0) + goto on_error; + + /* Detect overflow. */ + if (0xffffffff / 85 < acc || + 0xffffffff - de < (acc *= 85)) + goto on_error; + + acc += de; + + cnt = (output_len < 4) ? (int)output_len : 4; + output_len -= cnt; + do { + acc = (acc << 8) | (acc >> 24); + buf->ptr[buf->size++] = acc; + } while (--cnt); + } + + buf->ptr[buf->size] = 0; + + return 0; + +on_error: + buf->size = orig_size; + buf->ptr[buf->size] = '\0'; + + git_error_set(GIT_ERROR_INVALID, "invalid base85 input"); + return -1; +} + +#define HEX_DECODE(c) ((c | 32) % 39 - 9) + +int git_str_decode_percent( + git_str *buf, + const char *str, + size_t str_len) +{ + size_t str_pos, new_size; + + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, buf->size, str_len); + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, new_size, 1); + ENSURE_SIZE(buf, new_size); + + for (str_pos = 0; str_pos < str_len; buf->size++, str_pos++) { + if (str[str_pos] == '%' && + str_len > str_pos + 2 && + isxdigit(str[str_pos + 1]) && + isxdigit(str[str_pos + 2])) { + buf->ptr[buf->size] = (HEX_DECODE(str[str_pos + 1]) << 4) + + HEX_DECODE(str[str_pos + 2]); + str_pos += 2; + } else { + buf->ptr[buf->size] = str[str_pos]; + } + } + + buf->ptr[buf->size] = '\0'; + return 0; +} + +int git_str_vprintf(git_str *buf, const char *format, va_list ap) +{ + size_t expected_size, new_size; + int len; + + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&expected_size, strlen(format), 2); + GIT_ERROR_CHECK_ALLOC_ADD(&expected_size, expected_size, buf->size); + ENSURE_SIZE(buf, expected_size); + + while (1) { + va_list args; + va_copy(args, ap); + + len = p_vsnprintf( + buf->ptr + buf->size, + buf->asize - buf->size, + format, args + ); + + va_end(args); + + if (len < 0) { + git__free(buf->ptr); + buf->ptr = git_str__oom; + return -1; + } + + if ((size_t)len + 1 <= buf->asize - buf->size) { + buf->size += len; + break; + } + + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, buf->size, len); + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, new_size, 1); + ENSURE_SIZE(buf, new_size); + } + + return 0; +} + +int git_str_printf(git_str *buf, const char *format, ...) +{ + int r; + va_list ap; + + va_start(ap, format); + r = git_str_vprintf(buf, format, ap); + va_end(ap); + + return r; +} + +int git_str_copy_cstr(char *data, size_t datasize, const git_str *buf) +{ + size_t copylen; + + GIT_ASSERT_ARG(data); + GIT_ASSERT_ARG(datasize); + GIT_ASSERT_ARG(buf); + + data[0] = '\0'; + + if (buf->size == 0 || buf->asize <= 0) + return 0; + + copylen = buf->size; + if (copylen > datasize - 1) + copylen = datasize - 1; + memmove(data, buf->ptr, copylen); + data[copylen] = '\0'; + + return 0; +} + +void git_str_consume_bytes(git_str *buf, size_t len) +{ + git_str_consume(buf, buf->ptr + len); +} + +void git_str_consume(git_str *buf, const char *end) +{ + if (end > buf->ptr && end <= buf->ptr + buf->size) { + size_t consumed = end - buf->ptr; + memmove(buf->ptr, end, buf->size - consumed); + buf->size -= consumed; + buf->ptr[buf->size] = '\0'; + } +} + +void git_str_truncate(git_str *buf, size_t len) +{ + if (len >= buf->size) + return; + + buf->size = len; + if (buf->size < buf->asize) + buf->ptr[buf->size] = '\0'; +} + +void git_str_shorten(git_str *buf, size_t amount) +{ + if (buf->size > amount) + git_str_truncate(buf, buf->size - amount); + else + git_str_clear(buf); +} + +void git_str_truncate_at_char(git_str *buf, char separator) +{ + ssize_t idx = git_str_find(buf, separator); + if (idx >= 0) + git_str_truncate(buf, (size_t)idx); +} + +void git_str_rtruncate_at_char(git_str *buf, char separator) +{ + ssize_t idx = git_str_rfind_next(buf, separator); + git_str_truncate(buf, idx < 0 ? 0 : (size_t)idx); +} + +void git_str_swap(git_str *str_a, git_str *str_b) +{ + git_str t = *str_a; + *str_a = *str_b; + *str_b = t; +} + +char *git_str_detach(git_str *buf) +{ + char *data = buf->ptr; + + if (buf->asize == 0 || buf->ptr == git_str__oom) + return NULL; + + git_str_init(buf, 0); + + return data; +} + +int git_str_attach(git_str *buf, char *ptr, size_t asize) +{ + git_str_dispose(buf); + + if (ptr) { + buf->ptr = ptr; + buf->size = strlen(ptr); + if (asize) + buf->asize = (asize < buf->size) ? buf->size + 1 : asize; + else /* pass 0 to fall back on strlen + 1 */ + buf->asize = buf->size + 1; + } + + ENSURE_SIZE(buf, asize); + return 0; +} + +void git_str_attach_notowned(git_str *buf, const char *ptr, size_t size) +{ + if (git_str_is_allocated(buf)) + git_str_dispose(buf); + + if (!size) { + git_str_init(buf, 0); + } else { + buf->ptr = (char *)ptr; + buf->asize = 0; + buf->size = size; + } +} + +int git_str_join_n(git_str *buf, char separator, int nbuf, ...) +{ + va_list ap; + int i; + size_t total_size = 0, original_size = buf->size; + char *out, *original = buf->ptr; + + if (buf->size > 0 && buf->ptr[buf->size - 1] != separator) + ++total_size; /* space for initial separator */ + + /* Make two passes to avoid multiple reallocation */ + + va_start(ap, nbuf); + for (i = 0; i < nbuf; ++i) { + const char *segment; + size_t segment_len; + + segment = va_arg(ap, const char *); + if (!segment) + continue; + + segment_len = strlen(segment); + + GIT_ERROR_CHECK_ALLOC_ADD(&total_size, total_size, segment_len); + + if (segment_len == 0 || segment[segment_len - 1] != separator) + GIT_ERROR_CHECK_ALLOC_ADD(&total_size, total_size, 1); + } + va_end(ap); + + /* expand buffer if needed */ + if (total_size == 0) + return 0; + + GIT_ERROR_CHECK_ALLOC_ADD(&total_size, total_size, 1); + if (git_str_grow_by(buf, total_size) < 0) + return -1; + + out = buf->ptr + buf->size; + + /* append separator to existing buf if needed */ + if (buf->size > 0 && out[-1] != separator) + *out++ = separator; + + va_start(ap, nbuf); + for (i = 0; i < nbuf; ++i) { + const char *segment; + size_t segment_len; + + segment = va_arg(ap, const char *); + if (!segment) + continue; + + /* deal with join that references buffer's original content */ + if (segment >= original && segment < original + original_size) { + size_t offset = (segment - original); + segment = buf->ptr + offset; + segment_len = original_size - offset; + } else { + segment_len = strlen(segment); + } + + /* skip leading separators */ + if (out > buf->ptr && out[-1] == separator) + while (segment_len > 0 && *segment == separator) { + segment++; + segment_len--; + } + + /* copy over next buffer */ + if (segment_len > 0) { + memmove(out, segment, segment_len); + out += segment_len; + } + + /* append trailing separator (except for last item) */ + if (i < nbuf - 1 && out > buf->ptr && out[-1] != separator) + *out++ = separator; + } + va_end(ap); + + /* set size based on num characters actually written */ + buf->size = out - buf->ptr; + buf->ptr[buf->size] = '\0'; + + return 0; +} + +int git_str_join( + git_str *buf, + char separator, + const char *str_a, + const char *str_b) +{ + size_t strlen_a = str_a ? strlen(str_a) : 0; + size_t strlen_b = strlen(str_b); + size_t alloc_len; + int need_sep = 0; + ssize_t offset_a = -1; + + /* not safe to have str_b point internally to the buffer */ + if (buf->size) + GIT_ASSERT_ARG(str_b < buf->ptr || str_b >= buf->ptr + buf->size); + + /* figure out if we need to insert a separator */ + if (separator && strlen_a) { + while (*str_b == separator) { str_b++; strlen_b--; } + if (str_a[strlen_a - 1] != separator) + need_sep = 1; + } + + /* str_a could be part of the buffer */ + if (buf->size && str_a >= buf->ptr && str_a < buf->ptr + buf->size) + offset_a = str_a - buf->ptr; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, strlen_a, strlen_b); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, need_sep); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, 1); + ENSURE_SIZE(buf, alloc_len); + + /* fix up internal pointers */ + if (offset_a >= 0) + str_a = buf->ptr + offset_a; + + /* do the actual copying */ + if (offset_a != 0 && str_a) + memmove(buf->ptr, str_a, strlen_a); + if (need_sep) + buf->ptr[strlen_a] = separator; + memcpy(buf->ptr + strlen_a + need_sep, str_b, strlen_b); + + buf->size = strlen_a + strlen_b + need_sep; + buf->ptr[buf->size] = '\0'; + + return 0; +} + +int git_str_join3( + git_str *buf, + char separator, + const char *str_a, + const char *str_b, + const char *str_c) +{ + size_t len_a = strlen(str_a), + len_b = strlen(str_b), + len_c = strlen(str_c), + len_total; + int sep_a = 0, sep_b = 0; + char *tgt; + + /* for this function, disallow pointers into the existing buffer */ + GIT_ASSERT(str_a < buf->ptr || str_a >= buf->ptr + buf->size); + GIT_ASSERT(str_b < buf->ptr || str_b >= buf->ptr + buf->size); + GIT_ASSERT(str_c < buf->ptr || str_c >= buf->ptr + buf->size); + + if (separator) { + if (len_a > 0) { + while (*str_b == separator) { str_b++; len_b--; } + sep_a = (str_a[len_a - 1] != separator); + } + if (len_a > 0 || len_b > 0) + while (*str_c == separator) { str_c++; len_c--; } + if (len_b > 0) + sep_b = (str_b[len_b - 1] != separator); + } + + GIT_ERROR_CHECK_ALLOC_ADD(&len_total, len_a, sep_a); + GIT_ERROR_CHECK_ALLOC_ADD(&len_total, len_total, len_b); + GIT_ERROR_CHECK_ALLOC_ADD(&len_total, len_total, sep_b); + GIT_ERROR_CHECK_ALLOC_ADD(&len_total, len_total, len_c); + GIT_ERROR_CHECK_ALLOC_ADD(&len_total, len_total, 1); + ENSURE_SIZE(buf, len_total); + + tgt = buf->ptr; + + if (len_a) { + memcpy(tgt, str_a, len_a); + tgt += len_a; + } + if (sep_a) + *tgt++ = separator; + if (len_b) { + memcpy(tgt, str_b, len_b); + tgt += len_b; + } + if (sep_b) + *tgt++ = separator; + if (len_c) + memcpy(tgt, str_c, len_c); + + buf->size = len_a + sep_a + len_b + sep_b + len_c; + buf->ptr[buf->size] = '\0'; + + return 0; +} + +void git_str_rtrim(git_str *buf) +{ + while (buf->size > 0) { + if (!git__isspace(buf->ptr[buf->size - 1])) + break; + + buf->size--; + } + + if (buf->asize > buf->size) + buf->ptr[buf->size] = '\0'; +} + +int git_str_cmp(const git_str *a, const git_str *b) +{ + int result = memcmp(a->ptr, b->ptr, min(a->size, b->size)); + return (result != 0) ? result : + (a->size < b->size) ? -1 : (a->size > b->size) ? 1 : 0; +} + +int git_str_splice( + git_str *buf, + size_t where, + size_t nb_to_remove, + const char *data, + size_t nb_to_insert) +{ + char *splice_loc; + size_t new_size, alloc_size; + + GIT_ASSERT(buf); + GIT_ASSERT(where <= buf->size); + GIT_ASSERT(nb_to_remove <= buf->size - where); + + splice_loc = buf->ptr + where; + + /* Ported from git.git + * https://github.com/git/git/blob/16eed7c/strbuf.c#L159-176 + */ + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, (buf->size - nb_to_remove), nb_to_insert); + GIT_ERROR_CHECK_ALLOC_ADD(&alloc_size, new_size, 1); + ENSURE_SIZE(buf, alloc_size); + + memmove(splice_loc + nb_to_insert, + splice_loc + nb_to_remove, + buf->size - where - nb_to_remove); + + memcpy(splice_loc, data, nb_to_insert); + + buf->size = new_size; + buf->ptr[buf->size] = '\0'; + return 0; +} + +/* Quote per http://marc.info/?l=git&m=112927316408690&w=2 */ +int git_str_quote(git_str *buf) +{ + const char whitespace[] = { 'a', 'b', 't', 'n', 'v', 'f', 'r' }; + git_str quoted = GIT_STR_INIT; + size_t i = 0; + bool quote = false; + int error = 0; + + /* walk to the first char that needs quoting */ + if (buf->size && buf->ptr[0] == '!') + quote = true; + + for (i = 0; !quote && i < buf->size; i++) { + if (buf->ptr[i] == '"' || buf->ptr[i] == '\\' || + buf->ptr[i] < ' ' || buf->ptr[i] > '~') { + quote = true; + break; + } + } + + if (!quote) + goto done; + + git_str_putc("ed, '"'); + git_str_put("ed, buf->ptr, i); + + for (; i < buf->size; i++) { + /* whitespace - use the map above, which is ordered by ascii value */ + if (buf->ptr[i] >= '\a' && buf->ptr[i] <= '\r') { + git_str_putc("ed, '\\'); + git_str_putc("ed, whitespace[buf->ptr[i] - '\a']); + } + + /* double quote and backslash must be escaped */ + else if (buf->ptr[i] == '"' || buf->ptr[i] == '\\') { + git_str_putc("ed, '\\'); + git_str_putc("ed, buf->ptr[i]); + } + + /* escape anything unprintable as octal */ + else if (buf->ptr[i] != ' ' && + (buf->ptr[i] < '!' || buf->ptr[i] > '~')) { + git_str_printf("ed, "\\%03o", (unsigned char)buf->ptr[i]); + } + + /* yay, printable! */ + else { + git_str_putc("ed, buf->ptr[i]); + } + } + + git_str_putc("ed, '"'); + + if (git_str_oom("ed)) { + error = -1; + goto done; + } + + git_str_swap("ed, buf); + +done: + git_str_dispose("ed); + return error; +} + +/* Unquote per http://marc.info/?l=git&m=112927316408690&w=2 */ +int git_str_unquote(git_str *buf) +{ + size_t i, j; + char ch; + + git_str_rtrim(buf); + + if (buf->size < 2 || buf->ptr[0] != '"' || buf->ptr[buf->size-1] != '"') + goto invalid; + + for (i = 0, j = 1; j < buf->size-1; i++, j++) { + ch = buf->ptr[j]; + + if (ch == '\\') { + if (j == buf->size-2) + goto invalid; + + ch = buf->ptr[++j]; + + switch (ch) { + /* \" or \\ simply copy the char in */ + case '"': case '\\': + break; + + /* add the appropriate escaped char */ + case 'a': ch = '\a'; break; + case 'b': ch = '\b'; break; + case 'f': ch = '\f'; break; + case 'n': ch = '\n'; break; + case 'r': ch = '\r'; break; + case 't': ch = '\t'; break; + case 'v': ch = '\v'; break; + + /* \xyz digits convert to the char*/ + case '0': case '1': case '2': case '3': + if (j == buf->size-3) { + git_error_set(GIT_ERROR_INVALID, + "truncated quoted character \\%c", ch); + return -1; + } + + if (buf->ptr[j+1] < '0' || buf->ptr[j+1] > '7' || + buf->ptr[j+2] < '0' || buf->ptr[j+2] > '7') { + git_error_set(GIT_ERROR_INVALID, + "truncated quoted character \\%c%c%c", + buf->ptr[j], buf->ptr[j+1], buf->ptr[j+2]); + return -1; + } + + ch = ((buf->ptr[j] - '0') << 6) | + ((buf->ptr[j+1] - '0') << 3) | + (buf->ptr[j+2] - '0'); + j += 2; + break; + + default: + git_error_set(GIT_ERROR_INVALID, "invalid quoted character \\%c", ch); + return -1; + } + } + + buf->ptr[i] = ch; + } + + buf->ptr[i] = '\0'; + buf->size = i; + + return 0; + +invalid: + git_error_set(GIT_ERROR_INVALID, "invalid quoted line"); + return -1; +} + +int git_str_puts_escaped( + git_str *buf, + const char *string, + const char *esc_chars, + const char *esc_with) +{ + const char *scan; + size_t total = 0, esc_len = strlen(esc_with), count, alloclen; + + if (!string) + return 0; + + for (scan = string; *scan; ) { + /* count run of non-escaped characters */ + count = strcspn(scan, esc_chars); + total += count; + scan += count; + /* count run of escaped characters */ + count = strspn(scan, esc_chars); + total += count * (esc_len + 1); + scan += count; + } + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, total, 1); + if (git_str_grow_by(buf, alloclen) < 0) + return -1; + + for (scan = string; *scan; ) { + count = strcspn(scan, esc_chars); + + memmove(buf->ptr + buf->size, scan, count); + scan += count; + buf->size += count; + + for (count = strspn(scan, esc_chars); count > 0; --count) { + /* copy escape sequence */ + memmove(buf->ptr + buf->size, esc_with, esc_len); + buf->size += esc_len; + /* copy character to be escaped */ + buf->ptr[buf->size] = *scan; + buf->size++; + scan++; + } + } + + buf->ptr[buf->size] = '\0'; + + return 0; +} + +void git_str_unescape(git_str *buf) +{ + buf->size = git__unescape(buf->ptr); +} + +int git_str_crlf_to_lf(git_str *tgt, const git_str *src) +{ + const char *scan = src->ptr; + const char *scan_end = src->ptr + src->size; + const char *next = memchr(scan, '\r', src->size); + size_t new_size; + char *out; + + GIT_ASSERT(tgt != src); + + if (!next) + return git_str_set(tgt, src->ptr, src->size); + + /* reduce reallocs while in the loop */ + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, src->size, 1); + if (git_str_grow(tgt, new_size) < 0) + return -1; + + out = tgt->ptr; + tgt->size = 0; + + /* Find the next \r and copy whole chunk up to there to tgt */ + for (; next; scan = next + 1, next = memchr(scan, '\r', scan_end - scan)) { + if (next > scan) { + size_t copylen = (size_t)(next - scan); + memcpy(out, scan, copylen); + out += copylen; + } + + /* Do not drop \r unless it is followed by \n */ + if (next + 1 == scan_end || next[1] != '\n') + *out++ = '\r'; + } + + /* Copy remaining input into dest */ + if (scan < scan_end) { + size_t remaining = (size_t)(scan_end - scan); + memcpy(out, scan, remaining); + out += remaining; + } + + tgt->size = (size_t)(out - tgt->ptr); + tgt->ptr[tgt->size] = '\0'; + + return 0; +} + +int git_str_lf_to_crlf(git_str *tgt, const git_str *src) +{ + const char *start = src->ptr; + const char *end = start + src->size; + const char *scan = start; + const char *next = memchr(scan, '\n', src->size); + size_t alloclen; + + GIT_ASSERT(tgt != src); + + if (!next) + return git_str_set(tgt, src->ptr, src->size); + + /* attempt to reduce reallocs while in the loop */ + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, src->size, src->size >> 4); + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, 1); + if (git_str_grow(tgt, alloclen) < 0) + return -1; + tgt->size = 0; + + for (; next; scan = next + 1, next = memchr(scan, '\n', end - scan)) { + size_t copylen = next - scan; + + /* if we find mixed line endings, carry on */ + if (copylen && next[-1] == '\r') + copylen--; + + GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, copylen, 3); + if (git_str_grow_by(tgt, alloclen) < 0) + return -1; + + if (copylen) { + memcpy(tgt->ptr + tgt->size, scan, copylen); + tgt->size += copylen; + } + + tgt->ptr[tgt->size++] = '\r'; + tgt->ptr[tgt->size++] = '\n'; + } + + tgt->ptr[tgt->size] = '\0'; + return git_str_put(tgt, scan, end - scan); +} + +int git_str_common_prefix(git_str *buf, char *const *const strings, size_t count) +{ + size_t i; + const char *str, *pfx; + + git_str_clear(buf); + + if (!strings || !count) + return 0; + + /* initialize common prefix to first string */ + if (git_str_sets(buf, strings[0]) < 0) + return -1; + + /* go through the rest of the strings, truncating to shared prefix */ + for (i = 1; i < count; ++i) { + + for (str = strings[i], pfx = buf->ptr; + *str && *str == *pfx; + str++, pfx++) + /* scanning */; + + git_str_truncate(buf, pfx - buf->ptr); + + if (!buf->size) + break; + } + + return 0; +} + +int git_str_is_binary(const git_str *buf) +{ + const char *scan = buf->ptr, *end = buf->ptr + buf->size; + git_str_bom_t bom; + int printable = 0, nonprintable = 0; + + scan += git_str_detect_bom(&bom, buf); + + if (bom > GIT_STR_BOM_UTF8) + return 1; + + while (scan < end) { + unsigned char c = *scan++; + + /* Printable characters are those above SPACE (0x1F) excluding DEL, + * and including BS, ESC and FF. + */ + if ((c > 0x1F && c != 127) || c == '\b' || c == '\033' || c == '\014') + printable++; + else if (c == '\0') + return true; + else if (!git__isspace(c)) + nonprintable++; + } + + return ((printable >> 7) < nonprintable); +} + +int git_str_contains_nul(const git_str *buf) +{ + return (memchr(buf->ptr, '\0', buf->size) != NULL); +} + +int git_str_detect_bom(git_str_bom_t *bom, const git_str *buf) +{ + const char *ptr; + size_t len; + + *bom = GIT_STR_BOM_NONE; + /* need at least 2 bytes to look for any BOM */ + if (buf->size < 2) + return 0; + + ptr = buf->ptr; + len = buf->size; + + switch (*ptr++) { + case 0: + if (len >= 4 && ptr[0] == 0 && ptr[1] == '\xFE' && ptr[2] == '\xFF') { + *bom = GIT_STR_BOM_UTF32_BE; + return 4; + } + break; + case '\xEF': + if (len >= 3 && ptr[0] == '\xBB' && ptr[1] == '\xBF') { + *bom = GIT_STR_BOM_UTF8; + return 3; + } + break; + case '\xFE': + if (*ptr == '\xFF') { + *bom = GIT_STR_BOM_UTF16_BE; + return 2; + } + break; + case '\xFF': + if (*ptr != '\xFE') + break; + if (len >= 4 && ptr[1] == 0 && ptr[2] == 0) { + *bom = GIT_STR_BOM_UTF32_LE; + return 4; + } else { + *bom = GIT_STR_BOM_UTF16_LE; + return 2; + } + break; + default: + break; + } + + return 0; +} + +bool git_str_gather_text_stats( + git_str_text_stats *stats, const git_str *buf, bool skip_bom) +{ + const char *scan = buf->ptr, *end = buf->ptr + buf->size; + int skip; + + memset(stats, 0, sizeof(*stats)); + + /* BOM detection */ + skip = git_str_detect_bom(&stats->bom, buf); + if (skip_bom) + scan += skip; + + /* Ignore EOF character */ + if (buf->size > 0 && end[-1] == '\032') + end--; + + /* Counting loop */ + while (scan < end) { + unsigned char c = *scan++; + + if (c > 0x1F && c != 0x7F) + stats->printable++; + else switch (c) { + case '\0': + stats->nul++; + stats->nonprintable++; + break; + case '\n': + stats->lf++; + break; + case '\r': + stats->cr++; + if (scan < end && *scan == '\n') + stats->crlf++; + break; + case '\t': case '\f': case '\v': case '\b': case 0x1b: /*ESC*/ + stats->printable++; + break; + default: + stats->nonprintable++; + break; + } + } + + /* Treat files with a bare CR as binary */ + return (stats->cr != stats->crlf || stats->nul > 0 || + ((stats->printable >> 7) < stats->nonprintable)); +} diff --git a/src/util/str.h b/src/util/str.h new file mode 100644 index 0000000..588e6fc --- /dev/null +++ b/src/util/str.h @@ -0,0 +1,357 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_str_h__ +#define INCLUDE_str_h__ + +#include "git2_util.h" + +struct git_str { + char *ptr; + size_t asize; + size_t size; +}; + +typedef enum { + GIT_STR_BOM_NONE = 0, + GIT_STR_BOM_UTF8 = 1, + GIT_STR_BOM_UTF16_LE = 2, + GIT_STR_BOM_UTF16_BE = 3, + GIT_STR_BOM_UTF32_LE = 4, + GIT_STR_BOM_UTF32_BE = 5 +} git_str_bom_t; + +typedef struct { + git_str_bom_t bom; /* BOM found at head of text */ + unsigned int nul, cr, lf, crlf; /* NUL, CR, LF and CRLF counts */ + unsigned int printable, nonprintable; /* These are just approximations! */ +} git_str_text_stats; + +extern char git_str__initstr[]; +extern char git_str__oom[]; + +/* Use to initialize string buffer structure when git_str is on stack */ +#define GIT_STR_INIT { git_str__initstr, 0, 0 } + +/** + * Static initializer for git_str from static string buffer + */ +#define GIT_STR_INIT_CONST(str, len) { (char *)(str), 0, (size_t)(len) } + +GIT_INLINE(bool) git_str_is_allocated(const git_str *str) +{ + return (str->ptr != NULL && str->asize > 0); +} + +/** + * Initialize a git_str structure. + * + * For the cases where GIT_STR_INIT cannot be used to do static + * initialization. + */ +extern int git_str_init(git_str *str, size_t initial_size); + +extern void git_str_dispose(git_str *str); + +/** + * Resize the string buffer allocation to make more space. + * + * This will attempt to grow the string buffer to accommodate the target + * size. The bstring buffer's `ptr` will be replaced with a newly + * allocated block of data. Be careful so that memory allocated by the + * caller is not lost. As a special variant, if you pass `target_size` as + * 0 and the memory is not allocated by libgit2, this will allocate a new + * buffer of size `size` and copy the external data into it. + * + * Currently, this will never shrink a buffer, only expand it. + * + * If the allocation fails, this will return an error and the buffer will be + * marked as invalid for future operations, invaliding the contents. + * + * @param str The buffer to be resized; may or may not be allocated yet + * @param target_size The desired available size + * @return 0 on success, -1 on allocation failure + */ +int git_str_grow(git_str *str, size_t target_size); + +/** + * Resize the buffer allocation to make more space. + * + * This will attempt to grow the string buffer to accommodate the + * additional size. It is similar to `git_str_grow`, but performs the + * new size calculation, checking for overflow. + * + * Like `git_str_grow`, if this is a user-supplied string buffer, + * this will allocate a new string uffer. + */ +extern int git_str_grow_by(git_str *str, size_t additional_size); + +/** + * Attempt to grow the buffer to hold at least `target_size` bytes. + * + * If the allocation fails, this will return an error. If `mark_oom` is + * true, this will mark the string buffer as invalid for future + * operations; if false, existing string buffer content will be preserved, + * but calling code must handle that string buffer was not expanded. If + * `preserve_external` is true, then any existing data pointed to be + * `ptr` even if `asize` is zero will be copied into the newly allocated + * string buffer. + */ +extern int git_str_try_grow( + git_str *str, size_t target_size, bool mark_oom); + +extern void git_str_swap(git_str *str_a, git_str *str_b); +extern char *git_str_detach(git_str *str); +extern int git_str_attach(git_str *str, char *ptr, size_t asize); + +/* Populates a `git_str` where the contents are not "owned" by the string + * buffer, and calls to `git_str_dispose` will not free the given str. + */ +extern void git_str_attach_notowned( + git_str *str, const char *ptr, size_t size); + +/** + * Test if there have been any reallocation failures with this git_str. + * + * Any function that writes to a git_str can fail due to memory allocation + * issues. If one fails, the git_str will be marked with an OOM error and + * further calls to modify the string buffer will fail. Check + * git_str_oom() at the end of your sequence and it will be true if you + * ran out of memory at any point with that string buffer. + * + * @return false if no error, true if allocation error + */ +GIT_INLINE(bool) git_str_oom(const git_str *str) +{ + return (str->ptr == git_str__oom); +} + +/* + * Functions below that return int value error codes will return 0 on + * success or -1 on failure (which generally means an allocation failed). + * Using a git_str where the allocation has failed with result in -1 from + * all further calls using that string buffer. As a result, you can + * ignore the return code of these functions and call them in a series + * then just call git_str_oom at the end. + */ + +int git_str_set(git_str *str, const void *data, size_t datalen); + +int git_str_sets(git_str *str, const char *string); +int git_str_putc(git_str *str, char c); +int git_str_putcn(git_str *str, char c, size_t len); +int git_str_put(git_str *str, const char *data, size_t len); +int git_str_puts(git_str *str, const char *string); +int git_str_printf(git_str *str, const char *format, ...) GIT_FORMAT_PRINTF(2, 3); +int git_str_vprintf(git_str *str, const char *format, va_list ap); +void git_str_clear(git_str *str); +void git_str_consume_bytes(git_str *str, size_t len); +void git_str_consume(git_str *str, const char *end); +void git_str_truncate(git_str *str, size_t len); +void git_str_shorten(git_str *str, size_t amount); +void git_str_truncate_at_char(git_str *path, char separator); +void git_str_rtruncate_at_char(git_str *path, char separator); + +/** General join with separator */ +int git_str_join_n(git_str *str, char separator, int len, ...); +/** Fast join of two strings - first may legally point into `str` data */ +int git_str_join(git_str *str, char separator, const char *str_a, const char *str_b); +/** Fast join of three strings - cannot reference `str` data */ +int git_str_join3(git_str *str, char separator, const char *str_a, const char *str_b, const char *str_c); + +/** + * Join two strings as paths, inserting a slash between as needed. + * @return 0 on success, -1 on failure + */ +GIT_INLINE(int) git_str_joinpath(git_str *str, const char *a, const char *b) +{ + return git_str_join(str, '/', a, b); +} + +GIT_INLINE(const char *) git_str_cstr(const git_str *str) +{ + return str->ptr; +} + +GIT_INLINE(size_t) git_str_len(const git_str *str) +{ + return str->size; +} + +int git_str_copy_cstr(char *data, size_t datasize, const git_str *str); + +#define git_str_PUTS(str, cstr) git_str_put(str, cstr, sizeof(cstr) - 1) + +GIT_INLINE(ssize_t) git_str_rfind_next(const git_str *str, char ch) +{ + ssize_t idx = (ssize_t)str->size - 1; + while (idx >= 0 && str->ptr[idx] == ch) idx--; + while (idx >= 0 && str->ptr[idx] != ch) idx--; + return idx; +} + +GIT_INLINE(ssize_t) git_str_rfind(const git_str *str, char ch) +{ + ssize_t idx = (ssize_t)str->size - 1; + while (idx >= 0 && str->ptr[idx] != ch) idx--; + return idx; +} + +GIT_INLINE(ssize_t) git_str_find(const git_str *str, char ch) +{ + void *found = memchr(str->ptr, ch, str->size); + return found ? (ssize_t)((const char *)found - str->ptr) : -1; +} + +/* Remove whitespace from the end of the string buffer */ +void git_str_rtrim(git_str *str); + +int git_str_cmp(const git_str *a, const git_str *b); + +/* Quote and unquote a string buffer as specified in + * http://marc.info/?l=git&m=112927316408690&w=2 + */ +int git_str_quote(git_str *str); +int git_str_unquote(git_str *str); + +/* Write data as a hex string */ +int git_str_encode_hexstr(git_str *str, const char *data, size_t len); + +/* Write data as base64 encoded in string buffer */ +int git_str_encode_base64(git_str *str, const char *data, size_t len); +/* Decode the given bas64 and write the result to the string buffer */ +int git_str_decode_base64(git_str *str, const char *base64, size_t len); + +/* Write data as "base85" encoded in string buffer */ +int git_str_encode_base85(git_str *str, const char *data, size_t len); +/* Decode the given "base85" and write the result to the string buffer */ +int git_str_decode_base85(git_str *str, const char *base64, size_t len, size_t output_len); + +/* + * Decode the given percent-encoded string and write the result to the + * string buffer. + */ +int git_str_decode_percent(git_str *str, const char *encoded, size_t len); + +/* + * Insert, remove or replace a portion of the string buffer. + * + * @param str The string buffer to work with + * + * @param where The location in the string buffer where the transformation + * should be applied. + * + * @param nb_to_remove The number of chars to be removed. 0 to not + * remove any character in the string buffer. + * + * @param data A pointer to the data which should be inserted. + * + * @param nb_to_insert The number of chars to be inserted. 0 to not + * insert any character from the string buffer. + * + * @return 0 or an error code. + */ +int git_str_splice( + git_str *str, + size_t where, + size_t nb_to_remove, + const char *data, + size_t nb_to_insert); + +/** + * Append string to string buffer, prefixing each character from + * `esc_chars` with `esc_with` string. + * + * @param str String buffer to append data to + * @param string String to escape and append + * @param esc_chars Characters to be escaped + * @param esc_with String to insert in from of each found character + * @return 0 on success, <0 on failure (probably allocation problem) + */ +extern int git_str_puts_escaped( + git_str *str, + const char *string, + const char *esc_chars, + const char *esc_with); + +/** + * Append string escaping characters that are regex special + */ +GIT_INLINE(int) git_str_puts_escape_regex(git_str *str, const char *string) +{ + return git_str_puts_escaped(str, string, "^.[]$()|*+?{}\\", "\\"); +} + +/** + * Unescape all characters in a string buffer in place + * + * I.e. remove backslashes + */ +extern void git_str_unescape(git_str *str); + +/** + * Replace all \r\n with \n. + * + * @return 0 on success, -1 on memory error + */ +extern int git_str_crlf_to_lf(git_str *tgt, const git_str *src); + +/** + * Replace all \n with \r\n. Does not modify existing \r\n. + * + * @return 0 on success, -1 on memory error + */ +extern int git_str_lf_to_crlf(git_str *tgt, const git_str *src); + +/** + * Fill string buffer with the common prefix of a array of strings + * + * String buffer will be set to empty if there is no common prefix + */ +extern int git_str_common_prefix(git_str *buf, char *const *const strings, size_t count); + +/** + * Check if a string buffer begins with a UTF BOM + * + * @param bom Set to the type of BOM detected or GIT_BOM_NONE + * @param str String buffer in which to check the first bytes for a BOM + * @return Number of bytes of BOM data (or 0 if no BOM found) + */ +extern int git_str_detect_bom(git_str_bom_t *bom, const git_str *str); + +/** + * Gather stats for a piece of text + * + * Fill the `stats` structure with counts of unreadable characters, carriage + * returns, etc, so it can be used in heuristics. This automatically skips + * a trailing EOF (\032 character). Also it will look for a BOM at the + * start of the text and can be told to skip that as well. + * + * @param stats Structure to be filled in + * @param str Text to process + * @param skip_bom Exclude leading BOM from stats if true + * @return Does the string buffer heuristically look like binary data + */ +extern bool git_str_gather_text_stats( + git_str_text_stats *stats, const git_str *str, bool skip_bom); + +/** +* Check quickly if string buffer looks like it contains binary data +* +* @param str string buffer to check +* @return 1 if string buffer looks like non-text data +*/ +int git_str_is_binary(const git_str *str); + +/** +* Check quickly if buffer contains a NUL byte +* +* @param str string buffer to check +* @return 1 if string buffer contains a NUL byte +*/ +int git_str_contains_nul(const git_str *str); + +#endif diff --git a/src/util/strmap.c b/src/util/strmap.c new file mode 100644 index 0000000..c6e5b6d --- /dev/null +++ b/src/util/strmap.c @@ -0,0 +1,100 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "strmap.h" + +#define kmalloc git__malloc +#define kcalloc git__calloc +#define krealloc git__realloc +#define kreallocarray git__reallocarray +#define kfree git__free +#include "khash.h" + +__KHASH_TYPE(str, const char *, void *) + +__KHASH_IMPL(str, static kh_inline, const char *, void *, 1, kh_str_hash_func, kh_str_hash_equal) + +int git_strmap_new(git_strmap **out) +{ + *out = kh_init(str); + GIT_ERROR_CHECK_ALLOC(*out); + + return 0; +} + +void git_strmap_free(git_strmap *map) +{ + kh_destroy(str, map); +} + +void git_strmap_clear(git_strmap *map) +{ + kh_clear(str, map); +} + +size_t git_strmap_size(git_strmap *map) +{ + return kh_size(map); +} + +void *git_strmap_get(git_strmap *map, const char *key) +{ + size_t idx = kh_get(str, map, key); + if (idx == kh_end(map) || !kh_exist(map, idx)) + return NULL; + return kh_val(map, idx); +} + +int git_strmap_set(git_strmap *map, const char *key, void *value) +{ + size_t idx; + int rval; + + idx = kh_put(str, map, key, &rval); + if (rval < 0) + return -1; + + if (rval == 0) + kh_key(map, idx) = key; + + kh_val(map, idx) = value; + + return 0; +} + +int git_strmap_delete(git_strmap *map, const char *key) +{ + khiter_t idx = kh_get(str, map, key); + if (idx == kh_end(map)) + return GIT_ENOTFOUND; + kh_del(str, map, idx); + return 0; +} + +int git_strmap_exists(git_strmap *map, const char *key) +{ + return kh_get(str, map, key) != kh_end(map); +} + +int git_strmap_iterate(void **value, git_strmap *map, size_t *iter, const char **key) +{ + size_t i = *iter; + + while (i < map->n_buckets && !kh_exist(map, i)) + i++; + + if (i >= map->n_buckets) + return GIT_ITEROVER; + + if (key) + *key = kh_key(map, i); + if (value) + *value = kh_val(map, i); + *iter = ++i; + + return 0; +} diff --git a/src/util/strmap.h b/src/util/strmap.h new file mode 100644 index 0000000..b64d3dc --- /dev/null +++ b/src/util/strmap.h @@ -0,0 +1,131 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_strmap_h__ +#define INCLUDE_strmap_h__ + +#include "git2_util.h" + +/** A map with C strings as key. */ +typedef struct kh_str_s git_strmap; + +/** + * Allocate a new string map. + * + * @param out Pointer to the map that shall be allocated. + * @return 0 on success, an error code if allocation has failed. + */ +int git_strmap_new(git_strmap **out); + +/** + * Free memory associated with the map. + * + * Note that this function will _not_ free keys or values added + * to this map. + * + * @param map Pointer to the map that is to be free'd. May be + * `NULL`. + */ +void git_strmap_free(git_strmap *map); + +/** + * Clear all entries from the map. + * + * This function will remove all entries from the associated map. + * Memory associated with it will not be released, though. + * + * @param map Pointer to the map that shall be cleared. May be + * `NULL`. + */ +void git_strmap_clear(git_strmap *map); + +/** + * Return the number of elements in the map. + * + * @parameter map map containing the elements + * @return number of elements in the map + */ +size_t git_strmap_size(git_strmap *map); + +/** + * Return value associated with the given key. + * + * @param map map to search key in + * @param key key to search for + * @return value associated with the given key or NULL if the key was not found + */ +void *git_strmap_get(git_strmap *map, const char *key); + +/** + * Set the entry for key to value. + * + * If the map has no corresponding entry for the given key, a new + * entry will be created with the given value. If an entry exists + * already, its value will be updated to match the given value. + * + * @param map map to create new entry in + * @param key key to set + * @param value value to associate the key with; may be NULL + * @return zero if the key was successfully set, a negative error + * code otherwise + */ +int git_strmap_set(git_strmap *map, const char *key, void *value); + +/** + * Delete an entry from the map. + * + * Delete the given key and its value from the map. If no such + * key exists, this will do nothing. + * + * @param map map to delete key in + * @param key key to delete + * @return `0` if the key has been deleted, GIT_ENOTFOUND if no + * such key was found, a negative code in case of an + * error + */ +int git_strmap_delete(git_strmap *map, const char *key); + +/** + * Check whether a key exists in the given map. + * + * @param map map to query for the key + * @param key key to search for + * @return 0 if the key has not been found, 1 otherwise + */ +int git_strmap_exists(git_strmap *map, const char *key); + +/** + * Iterate over entries of the map. + * + * This functions allows to iterate over all key-value entries of + * the map. The current position is stored in the `iter` variable + * and should be initialized to `0` before the first call to this + * function. + * + * @param map map to iterate over + * @param value pointer to the variable where to store the current + * value. May be NULL. + * @param iter iterator storing the current position. Initialize + * with zero previous to the first call. + * @param key pointer to the variable where to store the current + * key. May be NULL. + * @return `0` if the next entry was correctly retrieved. + * GIT_ITEROVER if no entries are left. A negative error + * code otherwise. + */ +int git_strmap_iterate(void **value, git_strmap *map, size_t *iter, const char **key); + +#define git_strmap_foreach(h, kvar, vvar, code) { size_t __i = 0; \ + while (git_strmap_iterate((void **) &(vvar), h, &__i, &(kvar)) == 0) { \ + code; \ + } } + +#define git_strmap_foreach_value(h, vvar, code) { size_t __i = 0; \ + while (git_strmap_iterate((void **) &(vvar), h, &__i, NULL) == 0) { \ + code; \ + } } + +#endif diff --git a/src/util/strnlen.h b/src/util/strnlen.h new file mode 100644 index 0000000..eecfe3c --- /dev/null +++ b/src/util/strnlen.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_strlen_h__ +#define INCLUDE_strlen_h__ + +#if defined(__MINGW32__) || defined(__sun) || defined(__APPLE__) || defined(__MidnightBSD__) ||\ + (defined(_MSC_VER) && _MSC_VER < 1500) +# define NO_STRNLEN +#endif + +#ifdef NO_STRNLEN +GIT_INLINE(size_t) p_strnlen(const char *s, size_t maxlen) { + const char *end = memchr(s, 0, maxlen); + return end ? (size_t)(end - s) : maxlen; +} +#else +# define p_strnlen strnlen +#endif + +#endif diff --git a/src/util/thread.c b/src/util/thread.c new file mode 100644 index 0000000..bc7364f --- /dev/null +++ b/src/util/thread.c @@ -0,0 +1,140 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2_util.h" + +#if !defined(GIT_THREADS) + +#define TLSDATA_MAX 16 + +typedef struct { + void *value; + void (GIT_SYSTEM_CALL *destroy_fn)(void *); +} tlsdata_value; + +static tlsdata_value tlsdata_values[TLSDATA_MAX]; +static int tlsdata_cnt = 0; + +int git_tlsdata_init(git_tlsdata_key *key, void (GIT_SYSTEM_CALL *destroy_fn)(void *)) +{ + if (tlsdata_cnt >= TLSDATA_MAX) + return -1; + + tlsdata_values[tlsdata_cnt].value = NULL; + tlsdata_values[tlsdata_cnt].destroy_fn = destroy_fn; + + *key = tlsdata_cnt; + tlsdata_cnt++; + + return 0; +} + +int git_tlsdata_set(git_tlsdata_key key, void *value) +{ + if (key < 0 || key > tlsdata_cnt) + return -1; + + tlsdata_values[key].value = value; + return 0; +} + +void *git_tlsdata_get(git_tlsdata_key key) +{ + if (key < 0 || key > tlsdata_cnt) + return NULL; + + return tlsdata_values[key].value; +} + +int git_tlsdata_dispose(git_tlsdata_key key) +{ + void *value; + void (*destroy_fn)(void *) = NULL; + + if (key < 0 || key > tlsdata_cnt) + return -1; + + value = tlsdata_values[key].value; + destroy_fn = tlsdata_values[key].destroy_fn; + + tlsdata_values[key].value = NULL; + tlsdata_values[key].destroy_fn = NULL; + + if (value && destroy_fn) + destroy_fn(value); + + return 0; +} + +#elif defined(GIT_WIN32) + +int git_tlsdata_init(git_tlsdata_key *key, void (GIT_SYSTEM_CALL *destroy_fn)(void *)) +{ + DWORD fls_index = FlsAlloc(destroy_fn); + + if (fls_index == FLS_OUT_OF_INDEXES) + return -1; + + *key = fls_index; + return 0; +} + +int git_tlsdata_set(git_tlsdata_key key, void *value) +{ + if (!FlsSetValue(key, value)) + return -1; + + return 0; +} + +void *git_tlsdata_get(git_tlsdata_key key) +{ + return FlsGetValue(key); +} + +int git_tlsdata_dispose(git_tlsdata_key key) +{ + if (!FlsFree(key)) + return -1; + + return 0; +} + +#elif defined(_POSIX_THREADS) + +int git_tlsdata_init(git_tlsdata_key *key, void (GIT_SYSTEM_CALL *destroy_fn)(void *)) +{ + if (pthread_key_create(key, destroy_fn) != 0) + return -1; + + return 0; +} + +int git_tlsdata_set(git_tlsdata_key key, void *value) +{ + if (pthread_setspecific(key, value) != 0) + return -1; + + return 0; +} + +void *git_tlsdata_get(git_tlsdata_key key) +{ + return pthread_getspecific(key); +} + +int git_tlsdata_dispose(git_tlsdata_key key) +{ + if (pthread_key_delete(key) != 0) + return -1; + + return 0; +} + +#else +# error unknown threading model +#endif diff --git a/src/util/thread.h b/src/util/thread.h new file mode 100644 index 0000000..c32554b --- /dev/null +++ b/src/util/thread.h @@ -0,0 +1,480 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_thread_h__ +#define INCLUDE_thread_h__ + +#if defined(GIT_THREADS) + +#if defined(__clang__) + +# if (__clang_major__ < 3 || (__clang_major__ == 3 && __clang_minor__ < 1)) +# error Atomic primitives do not exist on this version of clang; configure libgit2 with -DUSE_THREADS=OFF +# else +# define GIT_BUILTIN_ATOMIC +# endif + +#elif defined(__GNUC__) + +# if (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)) +# error Atomic primitives do not exist on this version of gcc; configure libgit2 with -DUSE_THREADS=OFF +# elif (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7)) +# define GIT_BUILTIN_ATOMIC +# else +# define GIT_BUILTIN_SYNC +# endif + +#endif + +#endif /* GIT_THREADS */ + +/* Common operations even if threading has been disabled */ +typedef struct { +#if defined(GIT_WIN32) + volatile long val; +#else + volatile int val; +#endif +} git_atomic32; + +#ifdef GIT_ARCH_64 + +typedef struct { +#if defined(GIT_WIN32) + volatile __int64 val; +#else + volatile int64_t val; +#endif +} git_atomic64; + +typedef git_atomic64 git_atomic_ssize; + +#define git_atomic_ssize_set git_atomic64_set +#define git_atomic_ssize_add git_atomic64_add +#define git_atomic_ssize_get git_atomic64_get + +#else + +typedef git_atomic32 git_atomic_ssize; + +#define git_atomic_ssize_set git_atomic32_set +#define git_atomic_ssize_add git_atomic32_add +#define git_atomic_ssize_get git_atomic32_get + +#endif + +#ifdef GIT_THREADS + +#ifdef GIT_WIN32 +# include "win32/thread.h" +#else +# include "unix/pthread.h" +#endif + +/* + * Atomically sets the contents of *a to be val. + */ +GIT_INLINE(void) git_atomic32_set(git_atomic32 *a, int val) +{ +#if defined(GIT_WIN32) + InterlockedExchange(&a->val, (LONG)val); +#elif defined(GIT_BUILTIN_ATOMIC) + __atomic_store_n(&a->val, val, __ATOMIC_SEQ_CST); +#elif defined(GIT_BUILTIN_SYNC) + __sync_lock_test_and_set(&a->val, val); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +/* + * Atomically increments the contents of *a by 1, and stores the result back into *a. + * @return the result of the operation. + */ +GIT_INLINE(int) git_atomic32_inc(git_atomic32 *a) +{ +#if defined(GIT_WIN32) + return InterlockedIncrement(&a->val); +#elif defined(GIT_BUILTIN_ATOMIC) + return __atomic_add_fetch(&a->val, 1, __ATOMIC_SEQ_CST); +#elif defined(GIT_BUILTIN_SYNC) + return __sync_add_and_fetch(&a->val, 1); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +/* + * Atomically adds the contents of *a and addend, and stores the result back into *a. + * @return the result of the operation. + */ +GIT_INLINE(int) git_atomic32_add(git_atomic32 *a, int32_t addend) +{ +#if defined(GIT_WIN32) + return InterlockedAdd(&a->val, addend); +#elif defined(GIT_BUILTIN_ATOMIC) + return __atomic_add_fetch(&a->val, addend, __ATOMIC_SEQ_CST); +#elif defined(GIT_BUILTIN_SYNC) + return __sync_add_and_fetch(&a->val, addend); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +/* + * Atomically decrements the contents of *a by 1, and stores the result back into *a. + * @return the result of the operation. + */ +GIT_INLINE(int) git_atomic32_dec(git_atomic32 *a) +{ +#if defined(GIT_WIN32) + return InterlockedDecrement(&a->val); +#elif defined(GIT_BUILTIN_ATOMIC) + return __atomic_sub_fetch(&a->val, 1, __ATOMIC_SEQ_CST); +#elif defined(GIT_BUILTIN_SYNC) + return __sync_sub_and_fetch(&a->val, 1); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +/* + * Atomically gets the contents of *a. + * @return the contents of *a. + */ +GIT_INLINE(int) git_atomic32_get(git_atomic32 *a) +{ +#if defined(GIT_WIN32) + return (int)InterlockedCompareExchange(&a->val, 0, 0); +#elif defined(GIT_BUILTIN_ATOMIC) + return __atomic_load_n(&a->val, __ATOMIC_SEQ_CST); +#elif defined(GIT_BUILTIN_SYNC) + return __sync_val_compare_and_swap(&a->val, 0, 0); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +GIT_INLINE(void *) git_atomic__compare_and_swap( + void * volatile *ptr, void *oldval, void *newval) +{ +#if defined(GIT_WIN32) + return InterlockedCompareExchangePointer((volatile PVOID *)ptr, newval, oldval); +#elif defined(GIT_BUILTIN_ATOMIC) + void *foundval = oldval; + __atomic_compare_exchange(ptr, &foundval, &newval, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); + return foundval; +#elif defined(GIT_BUILTIN_SYNC) + return __sync_val_compare_and_swap(ptr, oldval, newval); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +GIT_INLINE(volatile void *) git_atomic__swap( + void * volatile *ptr, void *newval) +{ +#if defined(GIT_WIN32) + return InterlockedExchangePointer(ptr, newval); +#elif defined(GIT_BUILTIN_ATOMIC) + void * foundval = NULL; + __atomic_exchange(ptr, &newval, &foundval, __ATOMIC_SEQ_CST); + return foundval; +#elif defined(GIT_BUILTIN_SYNC) + return (volatile void *)__sync_lock_test_and_set(ptr, newval); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +GIT_INLINE(volatile void *) git_atomic__load(void * volatile *ptr) +{ +#if defined(GIT_WIN32) + void *newval = NULL, *oldval = NULL; + return (volatile void *)InterlockedCompareExchangePointer((volatile PVOID *)ptr, newval, oldval); +#elif defined(GIT_BUILTIN_ATOMIC) + return (volatile void *)__atomic_load_n(ptr, __ATOMIC_SEQ_CST); +#elif defined(GIT_BUILTIN_SYNC) + return (volatile void *)__sync_val_compare_and_swap(ptr, 0, 0); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +#ifdef GIT_ARCH_64 + +/* + * Atomically adds the contents of *a and addend, and stores the result back into *a. + * @return the result of the operation. + */ +GIT_INLINE(int64_t) git_atomic64_add(git_atomic64 *a, int64_t addend) +{ +#if defined(GIT_WIN32) + return InterlockedAdd64(&a->val, addend); +#elif defined(GIT_BUILTIN_ATOMIC) + return __atomic_add_fetch(&a->val, addend, __ATOMIC_SEQ_CST); +#elif defined(GIT_BUILTIN_SYNC) + return __sync_add_and_fetch(&a->val, addend); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +/* + * Atomically sets the contents of *a to be val. + */ +GIT_INLINE(void) git_atomic64_set(git_atomic64 *a, int64_t val) +{ +#if defined(GIT_WIN32) + InterlockedExchange64(&a->val, val); +#elif defined(GIT_BUILTIN_ATOMIC) + __atomic_store_n(&a->val, val, __ATOMIC_SEQ_CST); +#elif defined(GIT_BUILTIN_SYNC) + __sync_lock_test_and_set(&a->val, val); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +/* + * Atomically gets the contents of *a. + * @return the contents of *a. + */ +GIT_INLINE(int64_t) git_atomic64_get(git_atomic64 *a) +{ +#if defined(GIT_WIN32) + return (int64_t)InterlockedCompareExchange64(&a->val, 0, 0); +#elif defined(GIT_BUILTIN_ATOMIC) + return __atomic_load_n(&a->val, __ATOMIC_SEQ_CST); +#elif defined(GIT_BUILTIN_SYNC) + return __sync_val_compare_and_swap(&a->val, 0, 0); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +#endif + +#else + +#define git_threads_global_init git__noop + +#define git_thread unsigned int +#define git_thread_create(t, s, a) git__noop(t, s, a) +#define git_thread_join(i, s) git__noop_args(i, s) + +/* Pthreads Mutex */ +#define git_mutex unsigned int +#define git_mutex_init(a) git__noop_args(a) +#define git_mutex_init(a) git__noop_args(a) +#define git_mutex_lock(a) git__noop_args(a) +#define git_mutex_unlock(a) git__noop_args(a) +#define git_mutex_free(a) git__noop_args(a) + +/* Pthreads condition vars */ +#define git_cond unsigned int +#define git_cond_init(c) git__noop_args(c) +#define git_cond_free(c) git__noop_args(c) +#define git_cond_wait(c, l) git__noop_args(c, l) +#define git_cond_signal(c) git__noop_args(c) +#define git_cond_broadcast(c) git__noop_args(c) + +/* Pthreads rwlock */ +#define git_rwlock unsigned int +#define git_rwlock_init(a) git__noop_args(a) +#define git_rwlock_rdlock(a) git__noop_args(a) +#define git_rwlock_rdunlock(a) git__noop_args(a) +#define git_rwlock_wrlock(a) git__noop_args(a) +#define git_rwlock_wrunlock(a) git__noop_args(a) +#define git_rwlock_free(a) git__noop_args(a) + +#define GIT_RWLOCK_STATIC_INIT 0 + + +GIT_INLINE(void) git_atomic32_set(git_atomic32 *a, int val) +{ + a->val = val; +} + +GIT_INLINE(int) git_atomic32_inc(git_atomic32 *a) +{ + return ++a->val; +} + +GIT_INLINE(int) git_atomic32_add(git_atomic32 *a, int32_t addend) +{ + a->val += addend; + return a->val; +} + +GIT_INLINE(int) git_atomic32_dec(git_atomic32 *a) +{ + return --a->val; +} + +GIT_INLINE(int) git_atomic32_get(git_atomic32 *a) +{ + return (int)a->val; +} + +GIT_INLINE(void *) git_atomic__compare_and_swap( + void * volatile *ptr, void *oldval, void *newval) +{ + void *foundval = *ptr; + if (foundval == oldval) + *ptr = newval; + return foundval; +} + +GIT_INLINE(volatile void *) git_atomic__swap( + void * volatile *ptr, void *newval) +{ + volatile void *old = *ptr; + *ptr = newval; + return old; +} + +GIT_INLINE(volatile void *) git_atomic__load(void * volatile *ptr) +{ + return *ptr; +} + +#ifdef GIT_ARCH_64 + +GIT_INLINE(int64_t) git_atomic64_add(git_atomic64 *a, int64_t addend) +{ + a->val += addend; + return a->val; +} + +GIT_INLINE(void) git_atomic64_set(git_atomic64 *a, int64_t val) +{ + a->val = val; +} + +GIT_INLINE(int64_t) git_atomic64_get(git_atomic64 *a) +{ + return (int64_t)a->val; +} + +#endif + +#endif + +/* + * Atomically replace the contents of *ptr (if they are equal to oldval) with + * newval. ptr must point to a pointer or a value that is the same size as a + * pointer. This is semantically compatible with: + * + * #define git_atomic_compare_and_swap(ptr, oldval, newval) \ + * ({ \ + * void *foundval = *ptr; \ + * if (foundval == oldval) \ + * *ptr = newval; \ + * foundval; \ + * }) + * + * @return the original contents of *ptr. + */ +#define git_atomic_compare_and_swap(ptr, oldval, newval) \ + git_atomic__compare_and_swap((void * volatile *)ptr, oldval, newval) + +/* + * Atomically replace the contents of v with newval. v must be the same size as + * a pointer. This is semantically compatible with: + * + * #define git_atomic_swap(v, newval) \ + * ({ \ + * volatile void *old = v; \ + * v = newval; \ + * old; \ + * }) + * + * @return the original contents of v. + */ +#define git_atomic_swap(v, newval) \ + (void *)git_atomic__swap((void * volatile *)&(v), newval) + +/* + * Atomically reads the contents of v. v must be the same size as a pointer. + * This is semantically compatible with: + * + * #define git_atomic_load(v) v + * + * @return the contents of v. + */ +#define git_atomic_load(v) \ + (void *)git_atomic__load((void * volatile *)&(v)) + +#if defined(GIT_THREADS) + +# if defined(GIT_WIN32) +# define GIT_MEMORY_BARRIER MemoryBarrier() +# elif defined(GIT_BUILTIN_ATOMIC) +# define GIT_MEMORY_BARRIER __atomic_thread_fence(__ATOMIC_SEQ_CST) +# elif defined(GIT_BUILTIN_SYNC) +# define GIT_MEMORY_BARRIER __sync_synchronize() +# endif + +#else + +# define GIT_MEMORY_BARRIER /* noop */ + +#endif + +/* Thread-local data */ + +#if !defined(GIT_THREADS) +# define git_tlsdata_key int +#elif defined(GIT_WIN32) +# define git_tlsdata_key DWORD +#elif defined(_POSIX_THREADS) +# define git_tlsdata_key pthread_key_t +#else +# error unknown threading model +#endif + +/** + * Create a thread-local data key. The destroy function will be + * called upon thread exit. On some platforms, it may be called + * when all threads have deleted their keys. + * + * Note that the tlsdata functions do not set an error message on + * failure; this is because the error handling in libgit2 is itself + * handled by thread-local data storage. + * + * @param key the tlsdata key + * @param destroy_fn function pointer called upon thread exit + * @return 0 on success, non-zero on failure + */ +int git_tlsdata_init(git_tlsdata_key *key, void (GIT_SYSTEM_CALL *destroy_fn)(void *)); + +/** + * Set a the thread-local value for the given key. + * + * @param key the tlsdata key to store data on + * @param value the pointer to store + * @return 0 on success, non-zero on failure + */ +int git_tlsdata_set(git_tlsdata_key key, void *value); + +/** + * Get the thread-local value for the given key. + * + * @param key the tlsdata key to retrieve the value of + * @return the pointer stored with git_tlsdata_set + */ +void *git_tlsdata_get(git_tlsdata_key key); + +/** + * Delete the given thread-local key. + * + * @param key the tlsdata key to dispose + * @return 0 on success, non-zero on failure + */ +int git_tlsdata_dispose(git_tlsdata_key key); + +#endif diff --git a/src/util/tsort.c b/src/util/tsort.c new file mode 100644 index 0000000..2ef03d0 --- /dev/null +++ b/src/util/tsort.c @@ -0,0 +1,382 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2_util.h" + +/** + * An array-of-pointers implementation of Python's Timsort + * Based on code by Christopher Swenson under the MIT license + * + * Copyright (c) 2010 Christopher Swenson + * Copyright (c) 2011 Vicent Marti + */ + +#ifndef MAX +# define MAX(x,y) (((x) > (y) ? (x) : (y))) +#endif + +#ifndef MIN +# define MIN(x,y) (((x) < (y) ? (x) : (y))) +#endif + +static int binsearch( + void **dst, const void *x, size_t size, git__sort_r_cmp cmp, void *payload) +{ + int l, c, r; + void *lx, *cx; + + l = 0; + r = (int)size - 1; + c = r >> 1; + lx = dst[l]; + + /* check for beginning conditions */ + if (cmp(x, lx, payload) < 0) + return 0; + + else if (cmp(x, lx, payload) == 0) { + int i = 1; + while (cmp(x, dst[i], payload) == 0) + i++; + return i; + } + + /* guaranteed not to be >= rx */ + cx = dst[c]; + while (1) { + const int val = cmp(x, cx, payload); + if (val < 0) { + if (c - l <= 1) return c; + r = c; + } else if (val > 0) { + if (r - c <= 1) return c + 1; + l = c; + lx = cx; + } else { + do { + cx = dst[++c]; + } while (cmp(x, cx, payload) == 0); + return c; + } + c = l + ((r - l) >> 1); + cx = dst[c]; + } +} + +/* Binary insertion sort, but knowing that the first "start" entries are sorted. Used in timsort. */ +static void bisort( + void **dst, size_t start, size_t size, git__sort_r_cmp cmp, void *payload) +{ + size_t i; + void *x; + int location; + + for (i = start; i < size; i++) { + int j; + /* If this entry is already correct, just move along */ + if (cmp(dst[i - 1], dst[i], payload) <= 0) + continue; + + /* Else we need to find the right place, shift everything over, and squeeze in */ + x = dst[i]; + location = binsearch(dst, x, i, cmp, payload); + for (j = (int)i - 1; j >= location; j--) { + dst[j + 1] = dst[j]; + } + dst[location] = x; + } +} + + +/* timsort implementation, based on timsort.txt */ +struct tsort_run { + ssize_t start; + ssize_t length; +}; + +struct tsort_store { + size_t alloc; + git__sort_r_cmp cmp; + void *payload; + void **storage; +}; + +static void reverse_elements(void **dst, ssize_t start, ssize_t end) +{ + while (start < end) { + void *tmp = dst[start]; + dst[start] = dst[end]; + dst[end] = tmp; + + start++; + end--; + } +} + +static ssize_t count_run( + void **dst, ssize_t start, ssize_t size, struct tsort_store *store) +{ + ssize_t curr = start + 2; + + if (size - start == 1) + return 1; + + if (start >= size - 2) { + if (store->cmp(dst[size - 2], dst[size - 1], store->payload) > 0) { + void *tmp = dst[size - 1]; + dst[size - 1] = dst[size - 2]; + dst[size - 2] = tmp; + } + + return 2; + } + + if (store->cmp(dst[start], dst[start + 1], store->payload) <= 0) { + while (curr < size - 1 && + store->cmp(dst[curr - 1], dst[curr], store->payload) <= 0) + curr++; + + return curr - start; + } else { + while (curr < size - 1 && + store->cmp(dst[curr - 1], dst[curr], store->payload) > 0) + curr++; + + /* reverse in-place */ + reverse_elements(dst, start, curr - 1); + return curr - start; + } +} + +static size_t compute_minrun(size_t n) +{ + int r = 0; + while (n >= 64) { + r |= n & 1; + n >>= 1; + } + return n + r; +} + +static int check_invariant(struct tsort_run *stack, ssize_t stack_curr) +{ + if (stack_curr < 2) + return 1; + + else if (stack_curr == 2) { + const ssize_t A = stack[stack_curr - 2].length; + const ssize_t B = stack[stack_curr - 1].length; + return (A > B); + } else { + const ssize_t A = stack[stack_curr - 3].length; + const ssize_t B = stack[stack_curr - 2].length; + const ssize_t C = stack[stack_curr - 1].length; + return !((A <= B + C) || (B <= C)); + } +} + +static int resize(struct tsort_store *store, size_t new_size) +{ + if (store->alloc < new_size) { + void **tempstore; + + tempstore = git__reallocarray(store->storage, new_size, sizeof(void *)); + + /** + * Do not propagate on OOM; this will abort the sort and + * leave the array unsorted, but no error code will be + * raised + */ + if (tempstore == NULL) + return -1; + + store->storage = tempstore; + store->alloc = new_size; + } + + return 0; +} + +static void merge(void **dst, const struct tsort_run *stack, ssize_t stack_curr, struct tsort_store *store) +{ + const ssize_t A = stack[stack_curr - 2].length; + const ssize_t B = stack[stack_curr - 1].length; + const ssize_t curr = stack[stack_curr - 2].start; + + void **storage; + ssize_t i, j, k; + + if (resize(store, MIN(A, B)) < 0) + return; + + storage = store->storage; + + /* left merge */ + if (A < B) { + memcpy(storage, &dst[curr], A * sizeof(void *)); + i = 0; + j = curr + A; + + for (k = curr; k < curr + A + B; k++) { + if ((i < A) && (j < curr + A + B)) { + if (store->cmp(storage[i], dst[j], store->payload) <= 0) + dst[k] = storage[i++]; + else + dst[k] = dst[j++]; + } else if (i < A) { + dst[k] = storage[i++]; + } else + dst[k] = dst[j++]; + } + } else { + memcpy(storage, &dst[curr + A], B * sizeof(void *)); + i = B - 1; + j = curr + A - 1; + + for (k = curr + A + B - 1; k >= curr; k--) { + if ((i >= 0) && (j >= curr)) { + if (store->cmp(dst[j], storage[i], store->payload) > 0) + dst[k] = dst[j--]; + else + dst[k] = storage[i--]; + } else if (i >= 0) + dst[k] = storage[i--]; + else + dst[k] = dst[j--]; + } + } +} + +static ssize_t collapse(void **dst, struct tsort_run *stack, ssize_t stack_curr, struct tsort_store *store, ssize_t size) +{ + ssize_t A, B, C; + + while (1) { + /* if the stack only has one thing on it, we are done with the collapse */ + if (stack_curr <= 1) + break; + + /* if this is the last merge, just do it */ + if ((stack_curr == 2) && (stack[0].length + stack[1].length == size)) { + merge(dst, stack, stack_curr, store); + stack[0].length += stack[1].length; + stack_curr--; + break; + } + + /* check if the invariant is off for a stack of 2 elements */ + else if ((stack_curr == 2) && (stack[0].length <= stack[1].length)) { + merge(dst, stack, stack_curr, store); + stack[0].length += stack[1].length; + stack_curr--; + break; + } + else if (stack_curr == 2) + break; + + A = stack[stack_curr - 3].length; + B = stack[stack_curr - 2].length; + C = stack[stack_curr - 1].length; + + /* check first invariant */ + if (A <= B + C) { + if (A < C) { + merge(dst, stack, stack_curr - 1, store); + stack[stack_curr - 3].length += stack[stack_curr - 2].length; + stack[stack_curr - 2] = stack[stack_curr - 1]; + stack_curr--; + } else { + merge(dst, stack, stack_curr, store); + stack[stack_curr - 2].length += stack[stack_curr - 1].length; + stack_curr--; + } + } else if (B <= C) { + merge(dst, stack, stack_curr, store); + stack[stack_curr - 2].length += stack[stack_curr - 1].length; + stack_curr--; + } else + break; + } + + return stack_curr; +} + +#define PUSH_NEXT() do {\ + len = count_run(dst, curr, size, store);\ + run = minrun;\ + if (run > (ssize_t)size - curr) run = size - curr;\ + if (run > len) {\ + bisort(&dst[curr], len, run, cmp, payload);\ + len = run;\ + }\ + run_stack[stack_curr].start = curr;\ + run_stack[stack_curr++].length = len;\ + curr += len;\ + if (curr == (ssize_t)size) {\ + /* finish up */ \ + while (stack_curr > 1) { \ + merge(dst, run_stack, stack_curr, store); \ + run_stack[stack_curr - 2].length += run_stack[stack_curr - 1].length; \ + stack_curr--; \ + } \ + if (store->storage != NULL) {\ + git__free(store->storage);\ + store->storage = NULL;\ + }\ + return;\ + }\ +}\ +while (0) + +void git__tsort_r( + void **dst, size_t size, git__sort_r_cmp cmp, void *payload) +{ + struct tsort_store _store, *store = &_store; + struct tsort_run run_stack[128]; + + ssize_t stack_curr = 0; + ssize_t len, run; + ssize_t curr = 0; + ssize_t minrun; + + if (size < 64) { + bisort(dst, 1, size, cmp, payload); + return; + } + + /* compute the minimum run length */ + minrun = (ssize_t)compute_minrun(size); + + /* temporary storage for merges */ + store->alloc = 0; + store->storage = NULL; + store->cmp = cmp; + store->payload = payload; + + PUSH_NEXT(); + PUSH_NEXT(); + PUSH_NEXT(); + + while (1) { + if (!check_invariant(run_stack, stack_curr)) { + stack_curr = collapse(dst, run_stack, stack_curr, store, size); + continue; + } + + PUSH_NEXT(); + } +} + +static int tsort_r_cmp(const void *a, const void *b, void *payload) +{ + return ((git__tsort_cmp)payload)(a, b); +} + +void git__tsort(void **dst, size_t size, git__tsort_cmp cmp) +{ + git__tsort_r(dst, size, tsort_r_cmp, cmp); +} diff --git a/src/util/unix/map.c b/src/util/unix/map.c new file mode 100644 index 0000000..9330776 --- /dev/null +++ b/src/util/unix/map.c @@ -0,0 +1,76 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2_util.h" + +#if !defined(GIT_WIN32) && !defined(NO_MMAP) + +#include "map.h" +#include +#include +#include + +int git__page_size(size_t *page_size) +{ + long sc_page_size = sysconf(_SC_PAGE_SIZE); + if (sc_page_size < 0) { + git_error_set(GIT_ERROR_OS, "can't determine system page size"); + return -1; + } + *page_size = (size_t) sc_page_size; + return 0; +} + +int git__mmap_alignment(size_t *alignment) +{ + return git__page_size(alignment); +} + +int p_mmap(git_map *out, size_t len, int prot, int flags, int fd, off64_t offset) +{ + int mprot = PROT_READ; + int mflag = 0; + + GIT_MMAP_VALIDATE(out, len, prot, flags); + + out->data = NULL; + out->len = 0; + + if (prot & GIT_PROT_WRITE) + mprot |= PROT_WRITE; + + if ((flags & GIT_MAP_TYPE) == GIT_MAP_SHARED) + mflag = MAP_SHARED; + else if ((flags & GIT_MAP_TYPE) == GIT_MAP_PRIVATE) + mflag = MAP_PRIVATE; + else + mflag = MAP_SHARED; + + out->data = mmap(NULL, len, mprot, mflag, fd, offset); + + if (!out->data || out->data == MAP_FAILED) { + git_error_set(GIT_ERROR_OS, "failed to mmap. Could not write data"); + return -1; + } + + out->len = len; + + return 0; +} + +int p_munmap(git_map *map) +{ + GIT_ASSERT_ARG(map); + munmap(map->data, map->len); + map->data = NULL; + map->len = 0; + + return 0; +} + +#endif + diff --git a/src/util/unix/posix.h b/src/util/unix/posix.h new file mode 100644 index 0000000..778477e --- /dev/null +++ b/src/util/unix/posix.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_unix_posix_h__ +#define INCLUDE_unix_posix_h__ + +#include "git2_util.h" + +#include +#include +#include +#include +#include + +typedef int GIT_SOCKET; +#define INVALID_SOCKET -1 + +#define p_lseek(f,n,w) lseek(f, n, w) +#define p_fstat(f,b) fstat(f, b) +#define p_lstat(p,b) lstat(p,b) +#define p_stat(p,b) stat(p, b) + +#if defined(GIT_USE_STAT_MTIMESPEC) +# define st_atime_nsec st_atimespec.tv_nsec +# define st_mtime_nsec st_mtimespec.tv_nsec +# define st_ctime_nsec st_ctimespec.tv_nsec +#elif defined(GIT_USE_STAT_MTIM) +# define st_atime_nsec st_atim.tv_nsec +# define st_mtime_nsec st_mtim.tv_nsec +# define st_ctime_nsec st_ctim.tv_nsec +#elif !defined(GIT_USE_STAT_MTIME_NSEC) && defined(GIT_USE_NSEC) +# error GIT_USE_NSEC defined but unknown struct stat nanosecond type +#endif + +#define p_utimes(f, t) utimes(f, t) + +#define p_readlink(a, b, c) readlink(a, b, c) +#define p_symlink(o,n) symlink(o, n) +#define p_link(o,n) link(o, n) +#define p_unlink(p) unlink(p) +#define p_mkdir(p,m) mkdir(p, m) +extern char *p_realpath(const char *, char *); + +GIT_INLINE(int) p_fsync(int fd) +{ + p_fsync__cnt++; + return fsync(fd); +} + +#define p_recv(s,b,l,f) recv(s,b,l,f) +#define p_send(s,b,l,f) send(s,b,l,f) +#define p_inet_pton(a, b, c) inet_pton(a, b, c) + +#define p_strcasecmp(s1, s2) strcasecmp(s1, s2) +#define p_strncasecmp(s1, s2, c) strncasecmp(s1, s2, c) +#define p_vsnprintf(b, c, f, a) vsnprintf(b, c, f, a) +#define p_snprintf snprintf +#define p_chdir(p) chdir(p) +#define p_rmdir(p) rmdir(p) +#define p_access(p,m) access(p,m) +#define p_ftruncate(fd, sz) ftruncate(fd, sz) + +/* + * Pre-Android 5 did not implement a virtual filesystem atop FAT + * partitions for Unix permissions, which causes chmod to fail. However, + * Unix permissions have no effect on Android anyway as file permissions + * are not actually managed this way, so treating it as a no-op across + * all Android is safe. + */ +#ifdef __ANDROID__ +# define p_chmod(p,m) 0 +#else +# define p_chmod(p,m) chmod(p, m) +#endif + +/* see win32/posix.h for explanation about why this exists */ +#define p_lstat_posixly(p,b) lstat(p,b) + +#define p_localtime_r(c, r) localtime_r(c, r) +#define p_gmtime_r(c, r) gmtime_r(c, r) + +#define p_timeval timeval + +#ifdef GIT_USE_FUTIMENS +GIT_INLINE(int) p_futimes(int f, const struct p_timeval t[2]) +{ + struct timespec s[2]; + s[0].tv_sec = t[0].tv_sec; + s[0].tv_nsec = t[0].tv_usec * 1000; + s[1].tv_sec = t[1].tv_sec; + s[1].tv_nsec = t[1].tv_usec * 1000; + return futimens(f, s); +} +#else +# define p_futimes futimes +#endif + +#define p_pread(f, d, s, o) pread(f, d, s, o) +#define p_pwrite(f, d, s, o) pwrite(f, d, s, o) + +#endif diff --git a/src/util/unix/pthread.h b/src/util/unix/pthread.h new file mode 100644 index 0000000..55f4ae2 --- /dev/null +++ b/src/util/unix/pthread.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_unix_pthread_h__ +#define INCLUDE_unix_pthread_h__ + +typedef struct { + pthread_t thread; +} git_thread; + +GIT_INLINE(int) git_threads_global_init(void) { return 0; } + +#define git_thread_create(git_thread_ptr, start_routine, arg) \ + pthread_create(&(git_thread_ptr)->thread, NULL, start_routine, arg) +#define git_thread_join(git_thread_ptr, status) \ + pthread_join((git_thread_ptr)->thread, status) +#define git_thread_currentid() ((size_t)(pthread_self())) +#define git_thread_exit(retval) pthread_exit(retval) + +/* Git Mutex */ +#define git_mutex pthread_mutex_t +#define git_mutex_init(a) pthread_mutex_init(a, NULL) +#define git_mutex_lock(a) pthread_mutex_lock(a) +#define git_mutex_unlock(a) pthread_mutex_unlock(a) +#define git_mutex_free(a) pthread_mutex_destroy(a) + +/* Git condition vars */ +#define git_cond pthread_cond_t +#define git_cond_init(c) pthread_cond_init(c, NULL) +#define git_cond_free(c) pthread_cond_destroy(c) +#define git_cond_wait(c, l) pthread_cond_wait(c, l) +#define git_cond_signal(c) pthread_cond_signal(c) +#define git_cond_broadcast(c) pthread_cond_broadcast(c) + +/* Pthread (-ish) rwlock + * + * This differs from normal pthreads rwlocks in two ways: + * 1. Separate APIs for releasing read locks and write locks (as + * opposed to the pure POSIX API which only has one unlock fn) + * 2. You should not use recursive read locks (i.e. grabbing a read + * lock in a thread that already holds a read lock) because the + * Windows implementation doesn't support it + */ +#define git_rwlock pthread_rwlock_t +#define git_rwlock_init(a) pthread_rwlock_init(a, NULL) +#define git_rwlock_rdlock(a) pthread_rwlock_rdlock(a) +#define git_rwlock_rdunlock(a) pthread_rwlock_unlock(a) +#define git_rwlock_wrlock(a) pthread_rwlock_wrlock(a) +#define git_rwlock_wrunlock(a) pthread_rwlock_unlock(a) +#define git_rwlock_free(a) pthread_rwlock_destroy(a) +#define GIT_RWLOCK_STATIC_INIT PTHREAD_RWLOCK_INITIALIZER + +#endif diff --git a/src/util/unix/realpath.c b/src/util/unix/realpath.c new file mode 100644 index 0000000..9e31a63 --- /dev/null +++ b/src/util/unix/realpath.c @@ -0,0 +1,32 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2_util.h" + +#ifndef GIT_WIN32 + +#include +#include +#include +#include + +char *p_realpath(const char *pathname, char *resolved) +{ + char *ret; + if ((ret = realpath(pathname, resolved)) == NULL) + return NULL; + +#ifdef __OpenBSD__ + /* The OpenBSD realpath function behaves differently, + * figure out if the file exists */ + if (access(ret, F_OK) < 0) + ret = NULL; +#endif + return ret; +} + +#endif diff --git a/src/util/utf8.c b/src/util/utf8.c new file mode 100644 index 0000000..c566fdf --- /dev/null +++ b/src/util/utf8.c @@ -0,0 +1,150 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "utf8.h" + +#include "git2_util.h" + +/* + * git_utf8_iterate is taken from the utf8proc project, + * http://www.public-software-group.org/utf8proc + * + * Copyright (c) 2009 Public Software Group e. V., Berlin, Germany + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the ""Software""), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +static const uint8_t utf8proc_utf8class[256] = { + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +static int utf8_charlen(const uint8_t *str, size_t str_len) +{ + uint8_t length; + size_t i; + + length = utf8proc_utf8class[str[0]]; + if (!length) + return -1; + + if (str_len > 0 && length > str_len) + return -1; + + for (i = 1; i < length; i++) { + if ((str[i] & 0xC0) != 0x80) + return -1; + } + + return (int)length; +} + +int git_utf8_iterate(uint32_t *out, const char *_str, size_t str_len) +{ + const uint8_t *str = (const uint8_t *)_str; + uint32_t uc = 0; + int length; + + *out = 0; + + if ((length = utf8_charlen(str, str_len)) < 0) + return -1; + + switch (length) { + case 1: + uc = str[0]; + break; + case 2: + uc = ((str[0] & 0x1F) << 6) + (str[1] & 0x3F); + if (uc < 0x80) uc = -1; + break; + case 3: + uc = ((str[0] & 0x0F) << 12) + ((str[1] & 0x3F) << 6) + + (str[2] & 0x3F); + if (uc < 0x800 || (uc >= 0xD800 && uc < 0xE000) || + (uc >= 0xFDD0 && uc < 0xFDF0)) uc = -1; + break; + case 4: + uc = ((str[0] & 0x07) << 18) + ((str[1] & 0x3F) << 12) + + ((str[2] & 0x3F) << 6) + (str[3] & 0x3F); + if (uc < 0x10000 || uc >= 0x110000) uc = -1; + break; + default: + return -1; + } + + if ((uc & 0xFFFF) >= 0xFFFE) + return -1; + + *out = uc; + return length; +} + +size_t git_utf8_char_length(const char *_str, size_t str_len) +{ + const uint8_t *str = (const uint8_t *)_str; + size_t offset = 0, count = 0; + + while (offset < str_len) { + int length = utf8_charlen(str + offset, str_len - offset); + + if (length < 0) + length = 1; + + offset += length; + count++; + } + + return count; +} + +size_t git_utf8_valid_buf_length(const char *_str, size_t str_len) +{ + const uint8_t *str = (const uint8_t *)_str; + size_t offset = 0; + + while (offset < str_len) { + int length = utf8_charlen(str + offset, str_len - offset); + + if (length < 0) + break; + + offset += length; + } + + return offset; +} diff --git a/src/util/utf8.h b/src/util/utf8.h new file mode 100644 index 0000000..753ab07 --- /dev/null +++ b/src/util/utf8.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_utf8_h__ +#define INCLUDE_utf8_h__ + +#include "git2_util.h" + +/* + * Iterate through an UTF-8 string, yielding one codepoint at a time. + * + * @param out pointer where to store the current codepoint + * @param str current position in the string + * @param str_len size left in the string + * @return length in bytes of the read codepoint; -1 if the codepoint was invalid + */ +extern int git_utf8_iterate(uint32_t *out, const char *str, size_t str_len); + +/** + * Returns the number of characters in the given string. + * + * This function will count invalid codepoints; if any given byte is + * not part of a valid UTF-8 codepoint, then it will be counted toward + * the length in characters. + * + * In other words: + * 0x24 (U+0024 "$") has length 1 + * 0xc2 0xa2 (U+00A2 "¢") has length 1 + * 0x24 0xc2 0xa2 (U+0024 U+00A2 "$¢") has length 2 + * 0xf0 0x90 0x8d 0x88 (U+10348 "𐍈") has length 1 + * 0x24 0xc0 0xc1 0x34 (U+0024 "4) has length 4 + * + * @param str string to scan + * @param str_len size of the string + * @return length in characters of the string + */ +extern size_t git_utf8_char_length(const char *str, size_t str_len); + +/** + * Iterate through an UTF-8 string and stops after finding any invalid UTF-8 + * codepoints. + * + * @param str string to scan + * @param str_len size of the string + * @return length in bytes of the string that contains valid data + */ +extern size_t git_utf8_valid_buf_length(const char *str, size_t str_len); + +#endif diff --git a/src/util/util.c b/src/util/util.c new file mode 100644 index 0000000..c8e8303 --- /dev/null +++ b/src/util/util.c @@ -0,0 +1,824 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "util.h" + +#include "git2_util.h" + +#ifdef GIT_WIN32 +# include "win32/utf-conv.h" +# include "win32/w32_buffer.h" + +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# include + +# ifdef GIT_QSORT_MSC +# include +# endif +#endif + +#ifdef _MSC_VER +# include +#endif + +#if defined(hpux) || defined(__hpux) || defined(_hpux) +# include +#endif + +int git__strntol64(int64_t *result, const char *nptr, size_t nptr_len, const char **endptr, int base) +{ + const char *p; + int64_t n, nn, v; + int c, ovfl, neg, ndig; + + p = nptr; + neg = 0; + n = 0; + ndig = 0; + ovfl = 0; + + /* + * White space + */ + while (nptr_len && git__isspace(*p)) + p++, nptr_len--; + + if (!nptr_len) + goto Return; + + /* + * Sign + */ + if (*p == '-' || *p == '+') { + if (*p == '-') + neg = 1; + p++; + nptr_len--; + } + + if (!nptr_len) + goto Return; + + /* + * Automatically detect the base if none was given to us. + * Right now, we assume that a number starting with '0x' + * is hexadecimal and a number starting with '0' is + * octal. + */ + if (base == 0) { + if (*p != '0') + base = 10; + else if (nptr_len > 2 && (p[1] == 'x' || p[1] == 'X')) + base = 16; + else + base = 8; + } + + if (base < 0 || 36 < base) + goto Return; + + /* + * Skip prefix of '0x'-prefixed hexadecimal numbers. There is no + * need to do the same for '0'-prefixed octal numbers as a + * leading '0' does not have any impact. Also, if we skip a + * leading '0' in such a string, then we may end up with no + * digits left and produce an error later on which isn't one. + */ + if (base == 16 && nptr_len > 2 && p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) { + p += 2; + nptr_len -= 2; + } + + /* + * Non-empty sequence of digits + */ + for (; nptr_len > 0; p++,ndig++,nptr_len--) { + c = *p; + v = base; + if ('0'<=c && c<='9') + v = c - '0'; + else if ('a'<=c && c<='z') + v = c - 'a' + 10; + else if ('A'<=c && c<='Z') + v = c - 'A' + 10; + if (v >= base) + break; + v = neg ? -v : v; + if (git__multiply_int64_overflow(&nn, n, base) || git__add_int64_overflow(&n, nn, v)) { + ovfl = 1; + /* Keep on iterating until the end of this number */ + continue; + } + } + +Return: + if (ndig == 0) { + git_error_set(GIT_ERROR_INVALID, "failed to convert string to long: not a number"); + return -1; + } + + if (endptr) + *endptr = p; + + if (ovfl) { + git_error_set(GIT_ERROR_INVALID, "failed to convert string to long: overflow error"); + return -1; + } + + *result = n; + return 0; +} + +int git__strntol32(int32_t *result, const char *nptr, size_t nptr_len, const char **endptr, int base) +{ + const char *tmp_endptr; + int32_t tmp_int; + int64_t tmp_long; + int error; + + if ((error = git__strntol64(&tmp_long, nptr, nptr_len, &tmp_endptr, base)) < 0) + return error; + + tmp_int = tmp_long & 0xFFFFFFFF; + if (tmp_int != tmp_long) { + int len = (int)(tmp_endptr - nptr); + git_error_set(GIT_ERROR_INVALID, "failed to convert: '%.*s' is too large", len, nptr); + return -1; + } + + *result = tmp_int; + if (endptr) + *endptr = tmp_endptr; + + return error; +} + +int git__strcasecmp(const char *a, const char *b) +{ + while (*a && *b && git__tolower(*a) == git__tolower(*b)) + ++a, ++b; + return ((unsigned char)git__tolower(*a) - (unsigned char)git__tolower(*b)); +} + +int git__strcasesort_cmp(const char *a, const char *b) +{ + int cmp = 0; + + while (*a && *b) { + if (*a != *b) { + if (git__tolower(*a) != git__tolower(*b)) + break; + /* use case in sort order even if not in equivalence */ + if (!cmp) + cmp = (int)(*(const uint8_t *)a) - (int)(*(const uint8_t *)b); + } + + ++a, ++b; + } + + if (*a || *b) + return (unsigned char)git__tolower(*a) - (unsigned char)git__tolower(*b); + + return cmp; +} + +int git__strncasecmp(const char *a, const char *b, size_t sz) +{ + int al, bl; + + do { + al = (unsigned char)git__tolower(*a); + bl = (unsigned char)git__tolower(*b); + ++a, ++b; + } while (--sz && al && al == bl); + + return al - bl; +} + +void git__strntolower(char *str, size_t len) +{ + size_t i; + + for (i = 0; i < len; ++i) { + str[i] = (char)git__tolower(str[i]); + } +} + +void git__strtolower(char *str) +{ + git__strntolower(str, strlen(str)); +} + +GIT_INLINE(int) prefixcmp(const char *str, size_t str_n, const char *prefix, bool icase) +{ + int s, p; + + while (str_n--) { + s = (unsigned char)*str++; + p = (unsigned char)*prefix++; + + if (icase) { + s = git__tolower(s); + p = git__tolower(p); + } + + if (!p) + return 0; + + if (s != p) + return s - p; + } + + return (0 - *prefix); +} + +int git__prefixcmp(const char *str, const char *prefix) +{ + unsigned char s, p; + + while (1) { + p = *prefix++; + s = *str++; + + if (!p) + return 0; + + if (s != p) + return s - p; + } +} + +int git__prefixncmp(const char *str, size_t str_n, const char *prefix) +{ + return prefixcmp(str, str_n, prefix, false); +} + +int git__prefixcmp_icase(const char *str, const char *prefix) +{ + return prefixcmp(str, SIZE_MAX, prefix, true); +} + +int git__prefixncmp_icase(const char *str, size_t str_n, const char *prefix) +{ + return prefixcmp(str, str_n, prefix, true); +} + +int git__suffixcmp(const char *str, const char *suffix) +{ + size_t a = strlen(str); + size_t b = strlen(suffix); + if (a < b) + return -1; + return strcmp(str + (a - b), suffix); +} + +char *git__strtok(char **end, const char *sep) +{ + char *ptr = *end; + + while (*ptr && strchr(sep, *ptr)) + ++ptr; + + if (*ptr) { + char *start = ptr; + *end = start + 1; + + while (**end && !strchr(sep, **end)) + ++*end; + + if (**end) { + **end = '\0'; + ++*end; + } + + return start; + } + + return NULL; +} + +/* Similar to strtok, but does not collapse repeated tokens. */ +char *git__strsep(char **end, const char *sep) +{ + char *start = *end, *ptr = *end; + + while (*ptr && !strchr(sep, *ptr)) + ++ptr; + + if (*ptr) { + *end = ptr + 1; + *ptr = '\0'; + + return start; + } + + return NULL; +} + +size_t git__linenlen(const char *buffer, size_t buffer_len) +{ + char *nl = memchr(buffer, '\n', buffer_len); + return nl ? (size_t)(nl - buffer) + 1 : buffer_len; +} + +/* + * Adapted Not So Naive algorithm from http://www-igm.univ-mlv.fr/~lecroq/string/ + */ +const void * git__memmem(const void *haystack, size_t haystacklen, + const void *needle, size_t needlelen) +{ + const char *h, *n; + size_t j, k, l; + + if (needlelen > haystacklen || !haystacklen || !needlelen) + return NULL; + + h = (const char *) haystack, + n = (const char *) needle; + + if (needlelen == 1) + return memchr(haystack, *n, haystacklen); + + if (n[0] == n[1]) { + k = 2; + l = 1; + } else { + k = 1; + l = 2; + } + + j = 0; + while (j <= haystacklen - needlelen) { + if (n[1] != h[j + 1]) { + j += k; + } else { + if (memcmp(n + 2, h + j + 2, needlelen - 2) == 0 && + n[0] == h[j]) + return h + j; + j += l; + } + } + + return NULL; +} + +void git__hexdump(const char *buffer, size_t len) +{ + static const size_t LINE_WIDTH = 16; + + size_t line_count, last_line, i, j; + const char *line; + + line_count = (len / LINE_WIDTH); + last_line = (len % LINE_WIDTH); + + for (i = 0; i < line_count; ++i) { + printf("%08" PRIxZ " ", (i * LINE_WIDTH)); + + line = buffer + (i * LINE_WIDTH); + for (j = 0; j < LINE_WIDTH; ++j, ++line) { + printf("%02x ", (unsigned char)*line & 0xFF); + + if (j == (LINE_WIDTH / 2)) + printf(" "); + } + + printf(" |"); + + line = buffer + (i * LINE_WIDTH); + for (j = 0; j < LINE_WIDTH; ++j, ++line) + printf("%c", (*line >= 32 && *line <= 126) ? *line : '.'); + + printf("|\n"); + } + + if (last_line > 0) { + printf("%08" PRIxZ " ", (line_count * LINE_WIDTH)); + + line = buffer + (line_count * LINE_WIDTH); + for (j = 0; j < last_line; ++j, ++line) { + printf("%02x ", (unsigned char)*line & 0xFF); + + if (j == (LINE_WIDTH / 2)) + printf(" "); + } + + if (j < (LINE_WIDTH / 2)) + printf(" "); + for (j = 0; j < (LINE_WIDTH - last_line); ++j) + printf(" "); + + printf(" |"); + + line = buffer + (line_count * LINE_WIDTH); + for (j = 0; j < last_line; ++j, ++line) + printf("%c", (*line >= 32 && *line <= 126) ? *line : '.'); + + printf("|\n"); + } + + printf("\n"); +} + +#ifdef GIT_LEGACY_HASH +uint32_t git__hash(const void *key, int len, unsigned int seed) +{ + const uint32_t m = 0x5bd1e995; + const int r = 24; + uint32_t h = seed ^ len; + + const unsigned char *data = (const unsigned char *)key; + + while(len >= 4) { + uint32_t k = *(uint32_t *)data; + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + data += 4; + len -= 4; + } + + switch(len) { + case 3: h ^= data[2] << 16; + case 2: h ^= data[1] << 8; + case 1: h ^= data[0]; + h *= m; + }; + + h ^= h >> 13; + h *= m; + h ^= h >> 15; + + return h; +} +#else +/* + Cross-platform version of Murmurhash3 + http://code.google.com/p/smhasher/wiki/MurmurHash3 + by Austin Appleby (aappleby@gmail.com) + + This code is on the public domain. +*/ +uint32_t git__hash(const void *key, int len, uint32_t seed) +{ + +#define MURMUR_BLOCK() {\ + k1 *= c1; \ + k1 = git__rotl(k1,11);\ + k1 *= c2;\ + h1 ^= k1;\ + h1 = h1*3 + 0x52dce729;\ + c1 = c1*5 + 0x7b7d159c;\ + c2 = c2*5 + 0x6bce6396;\ +} + + const uint8_t *data = (const uint8_t*)key; + const int nblocks = len / 4; + + const uint32_t *blocks = (const uint32_t *)(data + nblocks * 4); + const uint8_t *tail = (const uint8_t *)(data + nblocks * 4); + + uint32_t h1 = 0x971e137b ^ seed; + uint32_t k1; + + uint32_t c1 = 0x95543787; + uint32_t c2 = 0x2ad7eb25; + + int i; + + for (i = -nblocks; i; i++) { + k1 = blocks[i]; + MURMUR_BLOCK(); + } + + k1 = 0; + + switch(len & 3) { + case 3: k1 ^= tail[2] << 16; + /* fall through */ + case 2: k1 ^= tail[1] << 8; + /* fall through */ + case 1: k1 ^= tail[0]; + MURMUR_BLOCK(); + } + + h1 ^= len; + h1 ^= h1 >> 16; + h1 *= 0x85ebca6b; + h1 ^= h1 >> 13; + h1 *= 0xc2b2ae35; + h1 ^= h1 >> 16; + + return h1; +} +#endif + +/** + * A modified `bsearch` from the BSD glibc. + * + * Copyright (c) 1990 Regents of the University of California. + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. [rescinded 22 July 1999] + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +int git__bsearch( + void **array, + size_t array_len, + const void *key, + int (*compare)(const void *, const void *), + size_t *position) +{ + size_t lim; + int cmp = -1; + void **part, **base = array; + + for (lim = array_len; lim != 0; lim >>= 1) { + part = base + (lim >> 1); + cmp = (*compare)(key, *part); + if (cmp == 0) { + base = part; + break; + } + if (cmp > 0) { /* key > p; take right partition */ + base = part + 1; + lim--; + } /* else take left partition */ + } + + if (position) + *position = (base - array); + + return (cmp == 0) ? 0 : GIT_ENOTFOUND; +} + +int git__bsearch_r( + void **array, + size_t array_len, + const void *key, + int (*compare_r)(const void *, const void *, void *), + void *payload, + size_t *position) +{ + size_t lim; + int cmp = -1; + void **part, **base = array; + + for (lim = array_len; lim != 0; lim >>= 1) { + part = base + (lim >> 1); + cmp = (*compare_r)(key, *part, payload); + if (cmp == 0) { + base = part; + break; + } + if (cmp > 0) { /* key > p; take right partition */ + base = part + 1; + lim--; + } /* else take left partition */ + } + + if (position) + *position = (base - array); + + return (cmp == 0) ? 0 : GIT_ENOTFOUND; +} + +/** + * A strcmp wrapper + * + * We don't want direct pointers to the CRT on Windows, we may + * get stdcall conflicts. + */ +int git__strcmp_cb(const void *a, const void *b) +{ + return strcmp((const char *)a, (const char *)b); +} + +int git__strcasecmp_cb(const void *a, const void *b) +{ + return strcasecmp((const char *)a, (const char *)b); +} + +int git__parse_bool(int *out, const char *value) +{ + /* A missing value means true */ + if (value == NULL || + !strcasecmp(value, "true") || + !strcasecmp(value, "yes") || + !strcasecmp(value, "on")) { + *out = 1; + return 0; + } + if (!strcasecmp(value, "false") || + !strcasecmp(value, "no") || + !strcasecmp(value, "off") || + value[0] == '\0') { + *out = 0; + return 0; + } + + return -1; +} + +size_t git__unescape(char *str) +{ + char *scan, *pos = str; + + if (!str) + return 0; + + for (scan = str; *scan; pos++, scan++) { + if (*scan == '\\' && *(scan + 1) != '\0') + scan++; /* skip '\' but include next char */ + if (pos != scan) + *pos = *scan; + } + + if (pos != scan) { + *pos = '\0'; + } + + return (pos - str); +} + +#if defined(GIT_QSORT_MSC) || defined(GIT_QSORT_BSD) +typedef struct { + git__sort_r_cmp cmp; + void *payload; +} git__qsort_r_glue; + +static int GIT_LIBGIT2_CALL git__qsort_r_glue_cmp( + void *payload, const void *a, const void *b) +{ + git__qsort_r_glue *glue = payload; + return glue->cmp(a, b, glue->payload); +} +#endif + + +#if !defined(GIT_QSORT_BSD) && \ + !defined(GIT_QSORT_GNU) && \ + !defined(GIT_QSORT_C11) && \ + !defined(GIT_QSORT_MSC) + +static void swap(uint8_t *a, uint8_t *b, size_t elsize) +{ + char tmp[256]; + + while (elsize) { + size_t n = elsize < sizeof(tmp) ? elsize : sizeof(tmp); + memcpy(tmp, a + elsize - n, n); + memcpy(a + elsize - n, b + elsize - n, n); + memcpy(b + elsize - n, tmp, n); + elsize -= n; + } +} + +static void insertsort( + void *els, size_t nel, size_t elsize, + git__sort_r_cmp cmp, void *payload) +{ + uint8_t *base = els; + uint8_t *end = base + nel * elsize; + uint8_t *i, *j; + + for (i = base + elsize; i < end; i += elsize) + for (j = i; j > base && cmp(j, j - elsize, payload) < 0; j -= elsize) + swap(j, j - elsize, elsize); +} + +#endif + +void git__qsort_r( + void *els, size_t nel, size_t elsize, git__sort_r_cmp cmp, void *payload) +{ +#if defined(GIT_QSORT_GNU) + qsort_r(els, nel, elsize, cmp, payload); +#elif defined(GIT_QSORT_C11) + qsort_s(els, nel, elsize, cmp, payload); +#elif defined(GIT_QSORT_BSD) + git__qsort_r_glue glue = { cmp, payload }; + qsort_r(els, nel, elsize, &glue, git__qsort_r_glue_cmp); +#elif defined(GIT_QSORT_MSC) + git__qsort_r_glue glue = { cmp, payload }; + qsort_s(els, nel, elsize, git__qsort_r_glue_cmp, &glue); +#else + insertsort(els, nel, elsize, cmp, payload); +#endif +} + +#ifdef GIT_WIN32 +int git__getenv(git_str *out, const char *name) +{ + wchar_t *wide_name = NULL, *wide_value = NULL; + DWORD value_len; + int error = -1; + + git_str_clear(out); + + if (git_utf8_to_16_alloc(&wide_name, name) < 0) + return -1; + + if ((value_len = GetEnvironmentVariableW(wide_name, NULL, 0)) > 0) { + wide_value = git__malloc(value_len * sizeof(wchar_t)); + GIT_ERROR_CHECK_ALLOC(wide_value); + + value_len = GetEnvironmentVariableW(wide_name, wide_value, value_len); + } + + if (value_len) + error = git_str_put_w(out, wide_value, value_len); + else if (GetLastError() == ERROR_SUCCESS || GetLastError() == ERROR_ENVVAR_NOT_FOUND) + error = GIT_ENOTFOUND; + else + git_error_set(GIT_ERROR_OS, "could not read environment variable '%s'", name); + + git__free(wide_name); + git__free(wide_value); + return error; +} +#else +int git__getenv(git_str *out, const char *name) +{ + const char *val = getenv(name); + + git_str_clear(out); + + if (!val) + return GIT_ENOTFOUND; + + return git_str_puts(out, val); +} +#endif + +/* + * By doing this in two steps we can at least get + * the function to be somewhat coherent, even + * with this disgusting nest of #ifdefs. + */ +#ifndef _SC_NPROCESSORS_ONLN +# ifdef _SC_NPROC_ONLN +# define _SC_NPROCESSORS_ONLN _SC_NPROC_ONLN +# elif defined _SC_CRAY_NCPU +# define _SC_NPROCESSORS_ONLN _SC_CRAY_NCPU +# endif +#endif + +int git__online_cpus(void) +{ +#ifdef _SC_NPROCESSORS_ONLN + long ncpus; +#endif + +#ifdef _WIN32 + SYSTEM_INFO info; + GetSystemInfo(&info); + + if ((int)info.dwNumberOfProcessors > 0) + return (int)info.dwNumberOfProcessors; +#elif defined(hpux) || defined(__hpux) || defined(_hpux) + struct pst_dynamic psd; + + if (!pstat_getdynamic(&psd, sizeof(psd), (size_t)1, 0)) + return (int)psd.psd_proc_cnt; +#endif + +#ifdef _SC_NPROCESSORS_ONLN + if ((ncpus = (long)sysconf(_SC_NPROCESSORS_ONLN)) > 0) + return (int)ncpus; +#endif + + return 1; +} diff --git a/src/util/util.h b/src/util/util.h new file mode 100644 index 0000000..7f178b1 --- /dev/null +++ b/src/util/util.h @@ -0,0 +1,396 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_util_h__ +#define INCLUDE_util_h__ + +#ifndef GIT_WIN32 +# include +#endif + +#include "str.h" +#include "git2_util.h" +#include "strnlen.h" +#include "thread.h" + +#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) +#define bitsizeof(x) (CHAR_BIT * sizeof(x)) +#define MSB(x, bits) ((x) & (~UINT64_C(0) << (bitsizeof(x) - (bits)))) +#ifndef min +# define min(a,b) ((a) < (b) ? (a) : (b)) +#endif +#ifndef max +# define max(a,b) ((a) > (b) ? (a) : (b)) +#endif + +#if defined(__GNUC__) +# define GIT_CONTAINER_OF(ptr, type, member) \ + __builtin_choose_expr( \ + __builtin_offsetof(type, member) == 0 && \ + __builtin_types_compatible_p(__typeof__(&((type *) 0)->member), __typeof__(ptr)), \ + ((type *) (ptr)), \ + (void)0) +#else +# define GIT_CONTAINER_OF(ptr, type, member) (type *)(ptr) +#endif + +/** + * Return the length of a constant string. + * We are aware that `strlen` performs the same task and is usually + * optimized away by the compiler, whilst being safer because it returns + * valid values when passed a pointer instead of a constant string; however + * this macro will transparently work with wide-char and single-char strings. + */ +#define CONST_STRLEN(x) ((sizeof(x)/sizeof(x[0])) - 1) + +#define STRCMP_CASESELECT(IGNORE_CASE, STR1, STR2) \ + ((IGNORE_CASE) ? strcasecmp((STR1), (STR2)) : strcmp((STR1), (STR2))) + +#define CASESELECT(IGNORE_CASE, ICASE, CASE) \ + ((IGNORE_CASE) ? (ICASE) : (CASE)) + +extern int git__prefixcmp(const char *str, const char *prefix); +extern int git__prefixcmp_icase(const char *str, const char *prefix); +extern int git__prefixncmp(const char *str, size_t str_n, const char *prefix); +extern int git__prefixncmp_icase(const char *str, size_t str_n, const char *prefix); +extern int git__suffixcmp(const char *str, const char *suffix); + +GIT_INLINE(int) git__signum(int val) +{ + return ((val > 0) - (val < 0)); +} + +extern int git__strntol32(int32_t *n, const char *buff, size_t buff_len, const char **end_buf, int base); +extern int git__strntol64(int64_t *n, const char *buff, size_t buff_len, const char **end_buf, int base); + + +extern void git__hexdump(const char *buffer, size_t n); +extern uint32_t git__hash(const void *key, int len, uint32_t seed); + +/* 32-bit cross-platform rotl */ +#ifdef _MSC_VER /* use built-in method in MSVC */ +# define git__rotl(v, s) (uint32_t)_rotl(v, s) +#else /* use bitops in GCC; with o2 this gets optimized to a rotl instruction */ +# define git__rotl(v, s) (uint32_t)(((uint32_t)(v) << (s)) | ((uint32_t)(v) >> (32 - (s)))) +#endif + +extern char *git__strtok(char **end, const char *sep); +extern char *git__strsep(char **end, const char *sep); + +extern void git__strntolower(char *str, size_t len); +extern void git__strtolower(char *str); + +#ifdef GIT_WIN32 +GIT_INLINE(int) git__tolower(int c) +{ + return (c >= 'A' && c <= 'Z') ? (c + 32) : c; +} +#else +# define git__tolower(a) tolower(a) +#endif + +extern size_t git__linenlen(const char *buffer, size_t buffer_len); + +GIT_INLINE(const char *) git__next_line(const char *s) +{ + while (*s && *s != '\n') s++; + while (*s == '\n' || *s == '\r') s++; + return s; +} + +GIT_INLINE(const void *) git__memrchr(const void *s, int c, size_t n) +{ + const unsigned char *cp; + + if (n != 0) { + cp = (unsigned char *)s + n; + do { + if (*(--cp) == (unsigned char)c) + return cp; + } while (--n != 0); + } + + return NULL; +} + +extern const void * git__memmem(const void *haystack, size_t haystacklen, + const void *needle, size_t needlelen); + +typedef int (*git__tsort_cmp)(const void *a, const void *b); + +extern void git__tsort(void **dst, size_t size, git__tsort_cmp cmp); + +typedef int (*git__sort_r_cmp)(const void *a, const void *b, void *payload); + +extern void git__tsort_r( + void **dst, size_t size, git__sort_r_cmp cmp, void *payload); + +extern void git__qsort_r( + void *els, size_t nel, size_t elsize, git__sort_r_cmp cmp, void *payload); + +/** + * @param position If non-NULL, this will be set to the position where the + * element is or would be inserted if not found. + * @return 0 if found; GIT_ENOTFOUND if not found + */ +extern int git__bsearch( + void **array, + size_t array_len, + const void *key, + int (*compare)(const void *key, const void *element), + size_t *position); + +extern int git__bsearch_r( + void **array, + size_t array_len, + const void *key, + int (*compare_r)(const void *key, const void *element, void *payload), + void *payload, + size_t *position); + +#define git__strcmp strcmp +#define git__strncmp strncmp + +extern int git__strcmp_cb(const void *a, const void *b); +extern int git__strcasecmp_cb(const void *a, const void *b); + +extern int git__strcasecmp(const char *a, const char *b); +extern int git__strncasecmp(const char *a, const char *b, size_t sz); + +extern int git__strcasesort_cmp(const char *a, const char *b); + +/* + * Compare some NUL-terminated `a` to a possibly non-NUL terminated + * `b` of length `b_len`; like `strncmp` but ensuring that + * `strlen(a) == b_len` as well. + */ +GIT_INLINE(int) git__strlcmp(const char *a, const char *b, size_t b_len) +{ + int cmp = strncmp(a, b, b_len); + return cmp ? cmp : (int)a[b_len]; +} + +typedef struct { + git_atomic32 refcount; + void *owner; +} git_refcount; + +typedef void (*git_refcount_freeptr)(void *r); + +#define GIT_REFCOUNT_INC(r) { \ + git_atomic32_inc(&(r)->rc.refcount); \ +} + +#define GIT_REFCOUNT_DEC(_r, do_free) { \ + git_refcount *r = &(_r)->rc; \ + int val = git_atomic32_dec(&r->refcount); \ + if (val <= 0 && r->owner == NULL) { do_free(_r); } \ +} + +#define GIT_REFCOUNT_OWN(r, o) { \ + (void)git_atomic_swap((r)->rc.owner, o); \ +} + +#define GIT_REFCOUNT_OWNER(r) git_atomic_load((r)->rc.owner) + +#define GIT_REFCOUNT_VAL(r) git_atomic32_get((r)->rc.refcount) + + +static signed char from_hex[] = { +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 00 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 10 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 20 */ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, /* 30 */ +-1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 40 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 50 */ +-1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 60 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 70 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 80 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 90 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* a0 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* b0 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* c0 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* d0 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* e0 */ +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* f0 */ +}; + +GIT_INLINE(int) git__fromhex(char h) +{ + return from_hex[(unsigned char) h]; +} + +GIT_INLINE(int) git__ishex(const char *str) +{ + unsigned i; + for (i=0; str[i] != '\0'; i++) + if (git__fromhex(str[i]) < 0) + return 0; + return 1; +} + +GIT_INLINE(size_t) git__size_t_bitmask(size_t v) +{ + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + + return v; +} + +GIT_INLINE(size_t) git__size_t_powerof2(size_t v) +{ + return git__size_t_bitmask(v) + 1; +} + +GIT_INLINE(bool) git__isupper(int c) +{ + return (c >= 'A' && c <= 'Z'); +} + +GIT_INLINE(bool) git__isalpha(int c) +{ + return ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')); +} + +GIT_INLINE(bool) git__isdigit(int c) +{ + return (c >= '0' && c <= '9'); +} + +GIT_INLINE(bool) git__isspace(int c) +{ + return (c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r' || c == '\v'); +} + +GIT_INLINE(bool) git__isspace_nonlf(int c) +{ + return (c == ' ' || c == '\t' || c == '\f' || c == '\r' || c == '\v'); +} + +GIT_INLINE(bool) git__iswildcard(int c) +{ + return (c == '*' || c == '?' || c == '['); +} + +GIT_INLINE(bool) git__isxdigit(int c) +{ + return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); +} + +/* + * Parse a string value as a boolean, just like Core Git does. + * + * Valid values for true are: 'true', 'yes', 'on' + * Valid values for false are: 'false', 'no', 'off' + */ +extern int git__parse_bool(int *out, const char *value); + +/* + * Unescapes a string in-place. + * + * Edge cases behavior: + * - "jackie\" -> "jacky\" + * - "chan\\" -> "chan\" + */ +extern size_t git__unescape(char *str); + +/* + * Safely zero-out memory, making sure that the compiler + * doesn't optimize away the operation. + */ +GIT_INLINE(void) git__memzero(void *data, size_t size) +{ +#ifdef _MSC_VER + SecureZeroMemory((PVOID)data, size); +#else + volatile uint8_t *scan = (volatile uint8_t *)data; + + while (size--) + *scan++ = 0x0; +#endif +} + +#ifdef GIT_WIN32 + +GIT_INLINE(uint64_t) git_time_monotonic(void) +{ + /* GetTickCount64 returns the number of milliseconds that have + * elapsed since the system was started. */ + return GetTickCount64(); +} + +#elif __APPLE__ + +#include +#include + +GIT_INLINE(uint64_t) git_time_monotonic(void) +{ + static double scaling_factor = 0; + + if (scaling_factor == 0) { + mach_timebase_info_data_t info; + + scaling_factor = mach_timebase_info(&info) == KERN_SUCCESS ? + ((double)info.numer / (double)info.denom) / 1.0E6 : + -1; + } else if (scaling_factor < 0) { + struct timeval tv; + + /* mach_timebase_info failed; fall back to gettimeofday */ + gettimeofday(&tv, NULL); + return (tv.tv_sec * 1000) + (tv.tv_usec / 1000); + } + + return (uint64_t)(mach_absolute_time() * scaling_factor); +} + +#elif defined(__amigaos4__) + +#include + +GIT_INLINE(uint64_t) git_time_monotonic(void) +{ + struct TimeVal tv; + ITimer->GetUpTime(&tv); + return (tv.Seconds * 1000) + (tv.Microseconds / 1000); +} + +#else + +#include + +GIT_INLINE(uint64_t) git_time_monotonic(void) +{ + struct timeval tv; + +#ifdef CLOCK_MONOTONIC + struct timespec tp; + if (clock_gettime(CLOCK_MONOTONIC, &tp) == 0) + return (tp.tv_sec * 1000) + (tp.tv_nsec / 1.0E6); +#endif + + /* Fall back to using gettimeofday */ + gettimeofday(&tv, NULL); + return (tv.tv_sec * 1000) + (tv.tv_usec / 1000); +} + +#endif + +extern int git__getenv(git_str *out, const char *name); + +extern int git__online_cpus(void); + +GIT_INLINE(int) git__noop(void) { return 0; } +GIT_INLINE(int) git__noop_args(void *a, ...) { GIT_UNUSED(a); return 0; } + +#include "alloc.h" + +#endif diff --git a/src/util/varint.c b/src/util/varint.c new file mode 100644 index 0000000..9ffc1d7 --- /dev/null +++ b/src/util/varint.c @@ -0,0 +1,43 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "varint.h" + +uintmax_t git_decode_varint(const unsigned char *bufp, size_t *varint_len) +{ + const unsigned char *buf = bufp; + unsigned char c = *buf++; + uintmax_t val = c & 127; + while (c & 128) { + val += 1; + if (!val || MSB(val, 7)) { + /* This is not a valid varint_len, so it signals + the error */ + *varint_len = 0; + return 0; /* overflow */ + } + c = *buf++; + val = (val << 7) + (c & 127); + } + *varint_len = buf - bufp; + return val; +} + +int git_encode_varint(unsigned char *buf, size_t bufsize, uintmax_t value) +{ + unsigned char varint[16]; + unsigned pos = sizeof(varint) - 1; + varint[pos] = value & 127; + while (value >>= 7) + varint[--pos] = 128 | (--value & 127); + if (buf) { + if (bufsize < (sizeof(varint) - pos)) + return -1; + memcpy(buf, varint + pos, sizeof(varint) - pos); + } + return sizeof(varint) - pos; +} diff --git a/src/util/varint.h b/src/util/varint.h new file mode 100644 index 0000000..79b8f55 --- /dev/null +++ b/src/util/varint.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_varint_h__ +#define INCLUDE_varint_h__ + +#include "git2_util.h" + +#include + +extern int git_encode_varint(unsigned char *, size_t, uintmax_t); +extern uintmax_t git_decode_varint(const unsigned char *, size_t *); + +#endif diff --git a/src/util/vector.c b/src/util/vector.c new file mode 100644 index 0000000..4a4bc8c --- /dev/null +++ b/src/util/vector.c @@ -0,0 +1,431 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "vector.h" + +#include "integer.h" + +/* In elements, not bytes */ +#define MIN_ALLOCSIZE 8 + +GIT_INLINE(size_t) compute_new_size(git_vector *v) +{ + size_t new_size = v->_alloc_size; + + /* Use a resize factor of 1.5, which is quick to compute using integer + * instructions and less than the golden ratio (1.618...) */ + if (new_size < MIN_ALLOCSIZE) + new_size = MIN_ALLOCSIZE; + else if (new_size <= (SIZE_MAX / 3) * 2) + new_size += new_size / 2; + else + new_size = SIZE_MAX; + + return new_size; +} + +GIT_INLINE(int) resize_vector(git_vector *v, size_t new_size) +{ + void *new_contents; + + if (new_size == 0) + return 0; + + new_contents = git__reallocarray(v->contents, new_size, sizeof(void *)); + GIT_ERROR_CHECK_ALLOC(new_contents); + + v->_alloc_size = new_size; + v->contents = new_contents; + + return 0; +} + +int git_vector_size_hint(git_vector *v, size_t size_hint) +{ + if (v->_alloc_size >= size_hint) + return 0; + return resize_vector(v, size_hint); +} + +int git_vector_dup(git_vector *v, const git_vector *src, git_vector_cmp cmp) +{ + GIT_ASSERT_ARG(v); + GIT_ASSERT_ARG(src); + + v->_alloc_size = 0; + v->contents = NULL; + v->_cmp = cmp ? cmp : src->_cmp; + v->length = src->length; + v->flags = src->flags; + if (cmp != src->_cmp) + git_vector_set_sorted(v, 0); + + if (src->length) { + size_t bytes; + GIT_ERROR_CHECK_ALLOC_MULTIPLY(&bytes, src->length, sizeof(void *)); + v->contents = git__malloc(bytes); + GIT_ERROR_CHECK_ALLOC(v->contents); + v->_alloc_size = src->length; + memcpy(v->contents, src->contents, bytes); + } + + return 0; +} + +void git_vector_free(git_vector *v) +{ + if (!v) + return; + + git__free(v->contents); + v->contents = NULL; + + v->length = 0; + v->_alloc_size = 0; +} + +void git_vector_free_deep(git_vector *v) +{ + size_t i; + + if (!v) + return; + + for (i = 0; i < v->length; ++i) { + git__free(v->contents[i]); + v->contents[i] = NULL; + } + + git_vector_free(v); +} + +int git_vector_init(git_vector *v, size_t initial_size, git_vector_cmp cmp) +{ + GIT_ASSERT_ARG(v); + + v->_alloc_size = 0; + v->_cmp = cmp; + v->length = 0; + v->flags = GIT_VECTOR_SORTED; + v->contents = NULL; + + return resize_vector(v, max(initial_size, MIN_ALLOCSIZE)); +} + +void **git_vector_detach(size_t *size, size_t *asize, git_vector *v) +{ + void **data = v->contents; + + if (size) + *size = v->length; + if (asize) + *asize = v->_alloc_size; + + v->_alloc_size = 0; + v->length = 0; + v->contents = NULL; + + return data; +} + +int git_vector_insert(git_vector *v, void *element) +{ + GIT_ASSERT_ARG(v); + + if (v->length >= v->_alloc_size && + resize_vector(v, compute_new_size(v)) < 0) + return -1; + + v->contents[v->length++] = element; + + git_vector_set_sorted(v, v->length <= 1); + + return 0; +} + +int git_vector_insert_sorted( + git_vector *v, void *element, int (*on_dup)(void **old, void *new)) +{ + int result; + size_t pos; + + GIT_ASSERT_ARG(v); + GIT_ASSERT(v->_cmp); + + if (!git_vector_is_sorted(v)) + git_vector_sort(v); + + if (v->length >= v->_alloc_size && + resize_vector(v, compute_new_size(v)) < 0) + return -1; + + /* If we find the element and have a duplicate handler callback, + * invoke it. If it returns non-zero, then cancel insert, otherwise + * proceed with normal insert. + */ + if (!git__bsearch(v->contents, v->length, element, v->_cmp, &pos) && + on_dup && (result = on_dup(&v->contents[pos], element)) < 0) + return result; + + /* shift elements to the right */ + if (pos < v->length) + memmove(v->contents + pos + 1, v->contents + pos, + (v->length - pos) * sizeof(void *)); + + v->contents[pos] = element; + v->length++; + + return 0; +} + +void git_vector_sort(git_vector *v) +{ + if (git_vector_is_sorted(v) || !v->_cmp) + return; + + if (v->length > 1) + git__tsort(v->contents, v->length, v->_cmp); + git_vector_set_sorted(v, 1); +} + +int git_vector_bsearch2( + size_t *at_pos, + git_vector *v, + git_vector_cmp key_lookup, + const void *key) +{ + GIT_ASSERT_ARG(v); + GIT_ASSERT_ARG(key); + GIT_ASSERT(key_lookup); + + /* need comparison function to sort the vector */ + if (!v->_cmp) + return -1; + + git_vector_sort(v); + + return git__bsearch(v->contents, v->length, key, key_lookup, at_pos); +} + +int git_vector_search2( + size_t *at_pos, const git_vector *v, git_vector_cmp key_lookup, const void *key) +{ + size_t i; + + GIT_ASSERT_ARG(v); + GIT_ASSERT_ARG(key); + GIT_ASSERT(key_lookup); + + for (i = 0; i < v->length; ++i) { + if (key_lookup(key, v->contents[i]) == 0) { + if (at_pos) + *at_pos = i; + + return 0; + } + } + + return GIT_ENOTFOUND; +} + +static int strict_comparison(const void *a, const void *b) +{ + return (a == b) ? 0 : -1; +} + +int git_vector_search(size_t *at_pos, const git_vector *v, const void *entry) +{ + return git_vector_search2(at_pos, v, v->_cmp ? v->_cmp : strict_comparison, entry); +} + +int git_vector_remove(git_vector *v, size_t idx) +{ + size_t shift_count; + + GIT_ASSERT_ARG(v); + + if (idx >= v->length) + return GIT_ENOTFOUND; + + shift_count = v->length - idx - 1; + + if (shift_count) + memmove(&v->contents[idx], &v->contents[idx + 1], + shift_count * sizeof(void *)); + + v->length--; + return 0; +} + +void git_vector_pop(git_vector *v) +{ + if (v->length > 0) + v->length--; +} + +void git_vector_uniq(git_vector *v, void (*git_free_cb)(void *)) +{ + git_vector_cmp cmp; + size_t i, j; + + if (v->length <= 1) + return; + + git_vector_sort(v); + cmp = v->_cmp ? v->_cmp : strict_comparison; + + for (i = 0, j = 1 ; j < v->length; ++j) + if (!cmp(v->contents[i], v->contents[j])) { + if (git_free_cb) + git_free_cb(v->contents[i]); + + v->contents[i] = v->contents[j]; + } else + v->contents[++i] = v->contents[j]; + + v->length -= j - i - 1; +} + +void git_vector_remove_matching( + git_vector *v, + int (*match)(const git_vector *v, size_t idx, void *payload), + void *payload) +{ + size_t i, j; + + for (i = 0, j = 0; j < v->length; ++j) { + v->contents[i] = v->contents[j]; + + if (!match(v, i, payload)) + i++; + } + + v->length = i; +} + +void git_vector_clear(git_vector *v) +{ + v->length = 0; + git_vector_set_sorted(v, 1); +} + +void git_vector_swap(git_vector *a, git_vector *b) +{ + git_vector t; + + if (a != b) { + memcpy(&t, a, sizeof(t)); + memcpy(a, b, sizeof(t)); + memcpy(b, &t, sizeof(t)); + } +} + +int git_vector_resize_to(git_vector *v, size_t new_length) +{ + if (new_length > v->_alloc_size && + resize_vector(v, new_length) < 0) + return -1; + + if (new_length > v->length) + memset(&v->contents[v->length], 0, + sizeof(void *) * (new_length - v->length)); + + v->length = new_length; + + return 0; +} + +int git_vector_insert_null(git_vector *v, size_t idx, size_t insert_len) +{ + size_t new_length; + + GIT_ASSERT_ARG(insert_len > 0); + GIT_ASSERT_ARG(idx <= v->length); + + GIT_ERROR_CHECK_ALLOC_ADD(&new_length, v->length, insert_len); + + if (new_length > v->_alloc_size && resize_vector(v, new_length) < 0) + return -1; + + memmove(&v->contents[idx + insert_len], &v->contents[idx], + sizeof(void *) * (v->length - idx)); + memset(&v->contents[idx], 0, sizeof(void *) * insert_len); + + v->length = new_length; + return 0; +} + +int git_vector_remove_range(git_vector *v, size_t idx, size_t remove_len) +{ + size_t new_length = v->length - remove_len; + size_t end_idx = 0; + + GIT_ASSERT_ARG(remove_len > 0); + + if (git__add_sizet_overflow(&end_idx, idx, remove_len)) + GIT_ASSERT(0); + + GIT_ASSERT(end_idx <= v->length); + + if (end_idx < v->length) + memmove(&v->contents[idx], &v->contents[end_idx], + sizeof(void *) * (v->length - end_idx)); + + memset(&v->contents[new_length], 0, sizeof(void *) * remove_len); + + v->length = new_length; + return 0; +} + +int git_vector_set(void **old, git_vector *v, size_t position, void *value) +{ + if (position + 1 > v->length) { + if (git_vector_resize_to(v, position + 1) < 0) + return -1; + } + + if (old != NULL) + *old = v->contents[position]; + + v->contents[position] = value; + + return 0; +} + +int git_vector_verify_sorted(const git_vector *v) +{ + size_t i; + + if (!git_vector_is_sorted(v)) + return -1; + + for (i = 1; i < v->length; ++i) { + if (v->_cmp(v->contents[i - 1], v->contents[i]) > 0) + return -1; + } + + return 0; +} + +void git_vector_reverse(git_vector *v) +{ + size_t a, b; + + if (v->length == 0) + return; + + a = 0; + b = v->length - 1; + + while (a < b) { + void *tmp = v->contents[a]; + v->contents[a] = v->contents[b]; + v->contents[b] = tmp; + a++; + b--; + } +} diff --git a/src/util/vector.h b/src/util/vector.h new file mode 100644 index 0000000..e50cdfe --- /dev/null +++ b/src/util/vector.h @@ -0,0 +1,128 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_vector_h__ +#define INCLUDE_vector_h__ + +#include "git2_util.h" + +typedef int (*git_vector_cmp)(const void *, const void *); + +enum { + GIT_VECTOR_SORTED = (1u << 0), + GIT_VECTOR_FLAG_MAX = (1u << 1) +}; + +typedef struct git_vector { + size_t _alloc_size; + git_vector_cmp _cmp; + void **contents; + size_t length; + uint32_t flags; +} git_vector; + +#define GIT_VECTOR_INIT {0} + +GIT_WARN_UNUSED_RESULT int git_vector_init( + git_vector *v, size_t initial_size, git_vector_cmp cmp); +void git_vector_free(git_vector *v); +void git_vector_free_deep(git_vector *v); /* free each entry and self */ +void git_vector_clear(git_vector *v); +GIT_WARN_UNUSED_RESULT int git_vector_dup( + git_vector *v, const git_vector *src, git_vector_cmp cmp); +void git_vector_swap(git_vector *a, git_vector *b); +int git_vector_size_hint(git_vector *v, size_t size_hint); + +void **git_vector_detach(size_t *size, size_t *asize, git_vector *v); + +void git_vector_sort(git_vector *v); + +/** Linear search for matching entry using internal comparison function */ +int git_vector_search(size_t *at_pos, const git_vector *v, const void *entry); + +/** Linear search for matching entry using explicit comparison function */ +int git_vector_search2(size_t *at_pos, const git_vector *v, git_vector_cmp cmp, const void *key); + +/** + * Binary search for matching entry using explicit comparison function that + * returns position where item would go if not found. + */ +int git_vector_bsearch2( + size_t *at_pos, git_vector *v, git_vector_cmp cmp, const void *key); + +/** Binary search for matching entry using internal comparison function */ +GIT_INLINE(int) git_vector_bsearch(size_t *at_pos, git_vector *v, const void *key) +{ + return git_vector_bsearch2(at_pos, v, v->_cmp, key); +} + +GIT_INLINE(void *) git_vector_get(const git_vector *v, size_t position) +{ + return (position < v->length) ? v->contents[position] : NULL; +} + +#define GIT_VECTOR_GET(V,I) ((I) < (V)->length ? (V)->contents[(I)] : NULL) + +GIT_INLINE(size_t) git_vector_length(const git_vector *v) +{ + return v->length; +} + +GIT_INLINE(void *) git_vector_last(const git_vector *v) +{ + return (v->length > 0) ? git_vector_get(v, v->length - 1) : NULL; +} + +#define git_vector_foreach(v, iter, elem) \ + for ((iter) = 0; (iter) < (v)->length && ((elem) = (v)->contents[(iter)], 1); (iter)++ ) + +#define git_vector_rforeach(v, iter, elem) \ + for ((iter) = (v)->length - 1; (iter) < SIZE_MAX && ((elem) = (v)->contents[(iter)], 1); (iter)-- ) + +int git_vector_insert(git_vector *v, void *element); +int git_vector_insert_sorted(git_vector *v, void *element, + int (*on_dup)(void **old, void *new)); +int git_vector_remove(git_vector *v, size_t idx); +void git_vector_pop(git_vector *v); +void git_vector_uniq(git_vector *v, void (*git_free_cb)(void *)); + +void git_vector_remove_matching( + git_vector *v, + int (*match)(const git_vector *v, size_t idx, void *payload), + void *payload); + +int git_vector_resize_to(git_vector *v, size_t new_length); +int git_vector_insert_null(git_vector *v, size_t idx, size_t insert_len); +int git_vector_remove_range(git_vector *v, size_t idx, size_t remove_len); + +int git_vector_set(void **old, git_vector *v, size_t position, void *value); + +/** Check if vector is sorted */ +#define git_vector_is_sorted(V) (((V)->flags & GIT_VECTOR_SORTED) != 0) + +/** Directly set sorted state of vector */ +#define git_vector_set_sorted(V,S) do { \ + (V)->flags = (S) ? ((V)->flags | GIT_VECTOR_SORTED) : \ + ((V)->flags & ~GIT_VECTOR_SORTED); } while (0) + +/** Set the comparison function used for sorting the vector */ +GIT_INLINE(void) git_vector_set_cmp(git_vector *v, git_vector_cmp cmp) +{ + if (cmp != v->_cmp) { + v->_cmp = cmp; + git_vector_set_sorted(v, 0); + } +} + +/* Just use this in tests, not for realz. returns -1 if not sorted */ +int git_vector_verify_sorted(const git_vector *v); + +/** + * Reverse the vector in-place. + */ +void git_vector_reverse(git_vector *v); + +#endif diff --git a/src/util/wildmatch.c b/src/util/wildmatch.c new file mode 100644 index 0000000..a894e48 --- /dev/null +++ b/src/util/wildmatch.c @@ -0,0 +1,320 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + * + * Do shell-style pattern matching for ?, \, [], and * characters. + * It is 8bit clean. + * + * Written by Rich $alz, mirror!rs, Wed Nov 26 19:03:17 EST 1986. + * Rich $alz is now . + * + * Modified by Wayne Davison to special-case '/' matching, to make '**' + * work differently than '*', and to fix the character-class code. + * + * Imported from git.git. + */ + +#include "wildmatch.h" + +#define GIT_SPACE 0x01 +#define GIT_DIGIT 0x02 +#define GIT_ALPHA 0x04 +#define GIT_GLOB_SPECIAL 0x08 +#define GIT_REGEX_SPECIAL 0x10 +#define GIT_PATHSPEC_MAGIC 0x20 +#define GIT_CNTRL 0x40 +#define GIT_PUNCT 0x80 + +enum { + S = GIT_SPACE, + A = GIT_ALPHA, + D = GIT_DIGIT, + G = GIT_GLOB_SPECIAL, /* *, ?, [, \\ */ + R = GIT_REGEX_SPECIAL, /* $, (, ), +, ., ^, {, | */ + P = GIT_PATHSPEC_MAGIC, /* other non-alnum, except for ] and } */ + X = GIT_CNTRL, + U = GIT_PUNCT, + Z = GIT_CNTRL | GIT_SPACE +}; + +static const unsigned char sane_ctype[256] = { + X, X, X, X, X, X, X, X, X, Z, Z, X, X, Z, X, X, /* 0.. 15 */ + X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, /* 16.. 31 */ + S, P, P, P, R, P, P, P, R, R, G, R, P, P, R, P, /* 32.. 47 */ + D, D, D, D, D, D, D, D, D, D, P, P, P, P, P, G, /* 48.. 63 */ + P, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, /* 64.. 79 */ + A, A, A, A, A, A, A, A, A, A, A, G, G, U, R, P, /* 80.. 95 */ + P, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, /* 96..111 */ + A, A, A, A, A, A, A, A, A, A, A, R, R, U, P, X, /* 112..127 */ + /* Nothing in the 128.. range */ +}; + +#define sane_istest(x,mask) ((sane_ctype[(unsigned char)(x)] & (mask)) != 0) +#define is_glob_special(x) sane_istest(x,GIT_GLOB_SPECIAL) + +typedef unsigned char uchar; + +/* What character marks an inverted character class? */ +#define NEGATE_CLASS '!' +#define NEGATE_CLASS2 '^' + +#define CC_EQ(class, len, litmatch) ((len) == sizeof (litmatch)-1 \ + && *(class) == *(litmatch) \ + && strncmp((char*)class, litmatch, len) == 0) + +#if defined STDC_HEADERS || !defined isascii +# define ISASCII(c) 1 +#else +# define ISASCII(c) isascii(c) +#endif + +#ifdef isblank +# define ISBLANK(c) (ISASCII(c) && isblank(c)) +#else +# define ISBLANK(c) ((c) == ' ' || (c) == '\t') +#endif + +#ifdef isgraph +# define ISGRAPH(c) (ISASCII(c) && isgraph(c)) +#else +# define ISGRAPH(c) (ISASCII(c) && isprint(c) && !isspace(c)) +#endif + +#define ISPRINT(c) (ISASCII(c) && isprint(c)) +#define ISDIGIT(c) (ISASCII(c) && isdigit(c)) +#define ISALNUM(c) (ISASCII(c) && isalnum(c)) +#define ISALPHA(c) (ISASCII(c) && isalpha(c)) +#define ISCNTRL(c) (ISASCII(c) && iscntrl(c)) +#define ISLOWER(c) (ISASCII(c) && islower(c)) +#define ISPUNCT(c) (ISASCII(c) && ispunct(c)) +#define ISSPACE(c) (ISASCII(c) && isspace(c)) +#define ISUPPER(c) (ISASCII(c) && isupper(c)) +#define ISXDIGIT(c) (ISASCII(c) && isxdigit(c)) + +/* Match pattern "p" against "text" */ +static int dowild(const uchar *p, const uchar *text, unsigned int flags) +{ + uchar p_ch; + const uchar *pattern = p; + + for ( ; (p_ch = *p) != '\0'; text++, p++) { + int matched, match_slash, negated; + uchar t_ch, prev_ch; + if ((t_ch = *text) == '\0' && p_ch != '*') + return WM_ABORT_ALL; + if ((flags & WM_CASEFOLD) && ISUPPER(t_ch)) + t_ch = tolower(t_ch); + if ((flags & WM_CASEFOLD) && ISUPPER(p_ch)) + p_ch = tolower(p_ch); + switch (p_ch) { + case '\\': + /* Literal match with following character. Note that the test + * in "default" handles the p[1] == '\0' failure case. */ + p_ch = *++p; + /* FALLTHROUGH */ + default: + if (t_ch != p_ch) + return WM_NOMATCH; + continue; + case '?': + /* Match anything but '/'. */ + if ((flags & WM_PATHNAME) && t_ch == '/') + return WM_NOMATCH; + continue; + case '*': + if (*++p == '*') { + const uchar *prev_p = p - 2; + while (*++p == '*') {} + if (!(flags & WM_PATHNAME)) + /* without WM_PATHNAME, '*' == '**' */ + match_slash = 1; + else if ((prev_p < pattern || *prev_p == '/') && + (*p == '\0' || *p == '/' || + (p[0] == '\\' && p[1] == '/'))) { + /* + * Assuming we already match 'foo/' and are at + * , just assume it matches + * nothing and go ahead match the rest of the + * pattern with the remaining string. This + * helps make foo/<*><*>/bar (<> because + * otherwise it breaks C comment syntax) match + * both foo/bar and foo/a/bar. + */ + if (p[0] == '/' && + dowild(p + 1, text, flags) == WM_MATCH) + return WM_MATCH; + match_slash = 1; + } else /* WM_PATHNAME is set */ + match_slash = 0; + } else + /* without WM_PATHNAME, '*' == '**' */ + match_slash = flags & WM_PATHNAME ? 0 : 1; + if (*p == '\0') { + /* Trailing "**" matches everything. Trailing "*" matches + * only if there are no more slash characters. */ + if (!match_slash) { + if (strchr((char*)text, '/') != NULL) + return WM_NOMATCH; + } + return WM_MATCH; + } else if (!match_slash && *p == '/') { + /* + * _one_ asterisk followed by a slash + * with WM_PATHNAME matches the next + * directory + */ + const char *slash = strchr((char*)text, '/'); + if (!slash) + return WM_NOMATCH; + text = (const uchar*)slash; + /* the slash is consumed by the top-level for loop */ + break; + } + while (1) { + if (t_ch == '\0') + break; + /* + * Try to advance faster when an asterisk is + * followed by a literal. We know in this case + * that the string before the literal + * must belong to "*". + * If match_slash is false, do not look past + * the first slash as it cannot belong to '*'. + */ + if (!is_glob_special(*p)) { + p_ch = *p; + if ((flags & WM_CASEFOLD) && ISUPPER(p_ch)) + p_ch = tolower(p_ch); + while ((t_ch = *text) != '\0' && + (match_slash || t_ch != '/')) { + if ((flags & WM_CASEFOLD) && ISUPPER(t_ch)) + t_ch = tolower(t_ch); + if (t_ch == p_ch) + break; + text++; + } + if (t_ch != p_ch) + return WM_NOMATCH; + } + if ((matched = dowild(p, text, flags)) != WM_NOMATCH) { + if (!match_slash || matched != WM_ABORT_TO_STARSTAR) + return matched; + } else if (!match_slash && t_ch == '/') + return WM_ABORT_TO_STARSTAR; + t_ch = *++text; + } + return WM_ABORT_ALL; + case '[': + p_ch = *++p; +#ifdef NEGATE_CLASS2 + if (p_ch == NEGATE_CLASS2) + p_ch = NEGATE_CLASS; +#endif + /* Assign literal 1/0 because of "matched" comparison. */ + negated = p_ch == NEGATE_CLASS ? 1 : 0; + if (negated) { + /* Inverted character class. */ + p_ch = *++p; + } + prev_ch = 0; + matched = 0; + do { + if (!p_ch) + return WM_ABORT_ALL; + if (p_ch == '\\') { + p_ch = *++p; + if (!p_ch) + return WM_ABORT_ALL; + if (t_ch == p_ch) + matched = 1; + } else if (p_ch == '-' && prev_ch && p[1] && p[1] != ']') { + p_ch = *++p; + if (p_ch == '\\') { + p_ch = *++p; + if (!p_ch) + return WM_ABORT_ALL; + } + if (t_ch <= p_ch && t_ch >= prev_ch) + matched = 1; + else if ((flags & WM_CASEFOLD) && ISLOWER(t_ch)) { + uchar t_ch_upper = toupper(t_ch); + if (t_ch_upper <= p_ch && t_ch_upper >= prev_ch) + matched = 1; + } + p_ch = 0; /* This makes "prev_ch" get set to 0. */ + } else if (p_ch == '[' && p[1] == ':') { + const uchar *s; + int i; + for (s = p += 2; (p_ch = *p) && p_ch != ']'; p++) {} /*SHARED ITERATOR*/ + if (!p_ch) + return WM_ABORT_ALL; + i = (int)(p - s - 1); + if (i < 0 || p[-1] != ':') { + /* Didn't find ":]", so treat like a normal set. */ + p = s - 2; + p_ch = '['; + if (t_ch == p_ch) + matched = 1; + continue; + } + if (CC_EQ(s,i, "alnum")) { + if (ISALNUM(t_ch)) + matched = 1; + } else if (CC_EQ(s,i, "alpha")) { + if (ISALPHA(t_ch)) + matched = 1; + } else if (CC_EQ(s,i, "blank")) { + if (ISBLANK(t_ch)) + matched = 1; + } else if (CC_EQ(s,i, "cntrl")) { + if (ISCNTRL(t_ch)) + matched = 1; + } else if (CC_EQ(s,i, "digit")) { + if (ISDIGIT(t_ch)) + matched = 1; + } else if (CC_EQ(s,i, "graph")) { + if (ISGRAPH(t_ch)) + matched = 1; + } else if (CC_EQ(s,i, "lower")) { + if (ISLOWER(t_ch)) + matched = 1; + } else if (CC_EQ(s,i, "print")) { + if (ISPRINT(t_ch)) + matched = 1; + } else if (CC_EQ(s,i, "punct")) { + if (ISPUNCT(t_ch)) + matched = 1; + } else if (CC_EQ(s,i, "space")) { + if (ISSPACE(t_ch)) + matched = 1; + } else if (CC_EQ(s,i, "upper")) { + if (ISUPPER(t_ch)) + matched = 1; + else if ((flags & WM_CASEFOLD) && ISLOWER(t_ch)) + matched = 1; + } else if (CC_EQ(s,i, "xdigit")) { + if (ISXDIGIT(t_ch)) + matched = 1; + } else /* malformed [:class:] string */ + return WM_ABORT_ALL; + p_ch = 0; /* This makes "prev_ch" get set to 0. */ + } else if (t_ch == p_ch) + matched = 1; + } while (prev_ch = p_ch, (p_ch = *++p) != ']'); + if (matched == negated || + ((flags & WM_PATHNAME) && t_ch == '/')) + return WM_NOMATCH; + continue; + } + } + + return *text ? WM_NOMATCH : WM_MATCH; +} + +/* Match the "pattern" against the "text" string. */ +int wildmatch(const char *pattern, const char *text, unsigned int flags) +{ + return dowild((const uchar*)pattern, (const uchar*)text, flags); +} diff --git a/src/util/wildmatch.h b/src/util/wildmatch.h new file mode 100644 index 0000000..f206405 --- /dev/null +++ b/src/util/wildmatch.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_wildmatch_h__ +#define INCLUDE_wildmatch_h__ + +#include "git2_util.h" + +#define WM_CASEFOLD 1 +#define WM_PATHNAME 2 + +#define WM_NOMATCH 1 +#define WM_MATCH 0 +#define WM_ABORT_ALL -1 +#define WM_ABORT_TO_STARSTAR -2 + +int wildmatch(const char *pattern, const char *text, unsigned int flags); + +#endif diff --git a/src/util/win32/dir.c b/src/util/win32/dir.c new file mode 100644 index 0000000..44052ca --- /dev/null +++ b/src/util/win32/dir.c @@ -0,0 +1,122 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "dir.h" + +#define GIT__WIN32_NO_WRAP_DIR +#include "posix.h" + +git__DIR *git__opendir(const char *dir) +{ + git_win32_path filter_w; + git__DIR *new = NULL; + size_t dirlen, alloclen; + + if (!dir || !git_win32__findfirstfile_filter(filter_w, dir)) + return NULL; + + dirlen = strlen(dir); + + if (GIT_ADD_SIZET_OVERFLOW(&alloclen, sizeof(*new), dirlen) || + GIT_ADD_SIZET_OVERFLOW(&alloclen, alloclen, 1) || + !(new = git__calloc(1, alloclen))) + return NULL; + + memcpy(new->dir, dir, dirlen); + + new->h = FindFirstFileW(filter_w, &new->f); + + if (new->h == INVALID_HANDLE_VALUE) { + git_error_set(GIT_ERROR_OS, "could not open directory '%s'", dir); + git__free(new); + return NULL; + } + + new->first = 1; + return new; +} + +int git__readdir_ext( + git__DIR *d, + struct git__dirent *entry, + struct git__dirent **result, + int *is_dir) +{ + if (!d || !entry || !result || d->h == INVALID_HANDLE_VALUE) + return -1; + + *result = NULL; + + if (d->first) + d->first = 0; + else if (!FindNextFileW(d->h, &d->f)) { + if (GetLastError() == ERROR_NO_MORE_FILES) + return 0; + git_error_set(GIT_ERROR_OS, "could not read from directory '%s'", d->dir); + return -1; + } + + /* Convert the path to UTF-8 */ + if (git_win32_path_to_utf8(entry->d_name, d->f.cFileName) < 0) + return -1; + + entry->d_ino = 0; + + *result = entry; + + if (is_dir != NULL) + *is_dir = ((d->f.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0); + + return 0; +} + +struct git__dirent *git__readdir(git__DIR *d) +{ + struct git__dirent *result; + if (git__readdir_ext(d, &d->entry, &result, NULL) < 0) + return NULL; + return result; +} + +void git__rewinddir(git__DIR *d) +{ + git_win32_path filter_w; + + if (!d) + return; + + if (d->h != INVALID_HANDLE_VALUE) { + FindClose(d->h); + d->h = INVALID_HANDLE_VALUE; + d->first = 0; + } + + if (!git_win32__findfirstfile_filter(filter_w, d->dir)) + return; + + d->h = FindFirstFileW(filter_w, &d->f); + + if (d->h == INVALID_HANDLE_VALUE) + git_error_set(GIT_ERROR_OS, "could not open directory '%s'", d->dir); + else + d->first = 1; +} + +int git__closedir(git__DIR *d) +{ + if (!d) + return 0; + + if (d->h != INVALID_HANDLE_VALUE) { + FindClose(d->h); + d->h = INVALID_HANDLE_VALUE; + } + + git__free(d); + return 0; +} + diff --git a/src/util/win32/dir.h b/src/util/win32/dir.h new file mode 100644 index 0000000..8101115 --- /dev/null +++ b/src/util/win32/dir.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_win32_dir_h__ +#define INCLUDE_win32_dir_h__ + +#include "git2_util.h" + +#include "w32_util.h" + +struct git__dirent { + int d_ino; + git_win32_utf8_path d_name; +}; + +typedef struct { + HANDLE h; + WIN32_FIND_DATAW f; + struct git__dirent entry; + int first; + char dir[GIT_FLEX_ARRAY]; +} git__DIR; + +extern git__DIR *git__opendir(const char *); +extern struct git__dirent *git__readdir(git__DIR *); +extern int git__readdir_ext( + git__DIR *, struct git__dirent *, struct git__dirent **, int *); +extern void git__rewinddir(git__DIR *); +extern int git__closedir(git__DIR *); + +# ifndef GIT__WIN32_NO_WRAP_DIR +# define dirent git__dirent +# define DIR git__DIR +# define opendir git__opendir +# define readdir git__readdir +# define readdir_r(d,e,r) git__readdir_ext((d),(e),(r),NULL) +# define rewinddir git__rewinddir +# define closedir git__closedir +# endif + +#endif diff --git a/src/util/win32/error.c b/src/util/win32/error.c new file mode 100644 index 0000000..dfd6fa1 --- /dev/null +++ b/src/util/win32/error.c @@ -0,0 +1,53 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "error.h" + +#include "utf-conv.h" + +#ifdef GIT_WINHTTP +# include +#endif + +char *git_win32_get_error_message(DWORD error_code) +{ + LPWSTR lpMsgBuf = NULL; + HMODULE hModule = NULL; + char *utf8_msg = NULL; + DWORD dwFlags = + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS; + + if (!error_code) + return NULL; + +#ifdef GIT_WINHTTP + /* Errors raised by WinHTTP are not in the system resource table */ + if (error_code >= WINHTTP_ERROR_BASE && + error_code <= WINHTTP_ERROR_LAST) + hModule = GetModuleHandleW(L"winhttp"); +#endif + + GIT_UNUSED(hModule); + + if (hModule) + dwFlags |= FORMAT_MESSAGE_FROM_HMODULE; + else + dwFlags |= FORMAT_MESSAGE_FROM_SYSTEM; + + if (FormatMessageW(dwFlags, hModule, error_code, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPWSTR)&lpMsgBuf, 0, NULL)) { + /* Convert the message to UTF-8. If this fails, we will + * return NULL, which is a condition expected by the caller */ + if (git_utf8_from_16_alloc(&utf8_msg, lpMsgBuf) < 0) + utf8_msg = NULL; + + LocalFree(lpMsgBuf); + } + + return utf8_msg; +} diff --git a/src/util/win32/error.h b/src/util/win32/error.h new file mode 100644 index 0000000..fd53b7f --- /dev/null +++ b/src/util/win32/error.h @@ -0,0 +1,15 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_win32_error_h__ +#define INCLUDE_win32_error_h__ + +#include "git2_util.h" + +extern char *git_win32_get_error_message(DWORD error_code); + +#endif diff --git a/src/util/win32/map.c b/src/util/win32/map.c new file mode 100644 index 0000000..52e1363 --- /dev/null +++ b/src/util/win32/map.c @@ -0,0 +1,141 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2_util.h" + +#include "map.h" +#include + +#ifndef NO_MMAP + +static DWORD get_page_size(void) +{ + static DWORD page_size; + SYSTEM_INFO sys; + + if (!page_size) { + GetSystemInfo(&sys); + page_size = sys.dwPageSize; + } + + return page_size; +} + +static DWORD get_allocation_granularity(void) +{ + static DWORD granularity; + SYSTEM_INFO sys; + + if (!granularity) { + GetSystemInfo(&sys); + granularity = sys.dwAllocationGranularity; + } + + return granularity; +} + +int git__page_size(size_t *page_size) +{ + *page_size = get_page_size(); + return 0; +} + +int git__mmap_alignment(size_t *page_size) +{ + *page_size = get_allocation_granularity(); + return 0; +} + +int p_mmap(git_map *out, size_t len, int prot, int flags, int fd, off64_t offset) +{ + HANDLE fh = (HANDLE)_get_osfhandle(fd); + DWORD alignment = get_allocation_granularity(); + DWORD fmap_prot = 0; + DWORD view_prot = 0; + DWORD off_low = 0; + DWORD off_hi = 0; + off64_t page_start; + off64_t page_offset; + + GIT_MMAP_VALIDATE(out, len, prot, flags); + + out->data = NULL; + out->len = 0; + out->fmh = NULL; + + if (fh == INVALID_HANDLE_VALUE) { + errno = EBADF; + git_error_set(GIT_ERROR_OS, "failed to mmap. Invalid handle value"); + return -1; + } + + if (prot & GIT_PROT_WRITE) + fmap_prot |= PAGE_READWRITE; + else if (prot & GIT_PROT_READ) + fmap_prot |= PAGE_READONLY; + + if (prot & GIT_PROT_WRITE) + view_prot |= FILE_MAP_WRITE; + if (prot & GIT_PROT_READ) + view_prot |= FILE_MAP_READ; + + page_start = (offset / alignment) * alignment; + page_offset = offset - page_start; + + if (page_offset != 0) { /* offset must be multiple of the allocation granularity */ + errno = EINVAL; + git_error_set(GIT_ERROR_OS, "failed to mmap. Offset must be multiple of allocation granularity"); + return -1; + } + + out->fmh = CreateFileMapping(fh, NULL, fmap_prot, 0, 0, NULL); + if (!out->fmh || out->fmh == INVALID_HANDLE_VALUE) { + git_error_set(GIT_ERROR_OS, "failed to mmap. Invalid handle value"); + out->fmh = NULL; + return -1; + } + + off_low = (DWORD)(page_start); + off_hi = (DWORD)(page_start >> 32); + out->data = MapViewOfFile(out->fmh, view_prot, off_hi, off_low, len); + if (!out->data) { + git_error_set(GIT_ERROR_OS, "failed to mmap. No data written"); + CloseHandle(out->fmh); + out->fmh = NULL; + return -1; + } + out->len = len; + + return 0; +} + +int p_munmap(git_map *map) +{ + int error = 0; + + GIT_ASSERT_ARG(map); + + if (map->data) { + if (!UnmapViewOfFile(map->data)) { + git_error_set(GIT_ERROR_OS, "failed to munmap. Could not unmap view of file"); + error = -1; + } + map->data = NULL; + } + + if (map->fmh) { + if (!CloseHandle(map->fmh)) { + git_error_set(GIT_ERROR_OS, "failed to munmap. Could not close handle"); + error = -1; + } + map->fmh = NULL; + } + + return error; +} + +#endif diff --git a/src/util/win32/mingw-compat.h b/src/util/win32/mingw-compat.h new file mode 100644 index 0000000..aa2bef9 --- /dev/null +++ b/src/util/win32/mingw-compat.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_win32_mingw_compat_h__ +#define INCLUDE_win32_mingw_compat_h__ + +#if defined(__MINGW32__) + +#undef stat + +#if _WIN32_WINNT < 0x0600 && !defined(__MINGW64_VERSION_MAJOR) +#undef MemoryBarrier +void __mingworg_MemoryBarrier(void); +#define MemoryBarrier __mingworg_MemoryBarrier +#define VOLUME_NAME_DOS 0x0 +#endif + +#endif + +#endif diff --git a/src/util/win32/msvc-compat.h b/src/util/win32/msvc-compat.h new file mode 100644 index 0000000..03f9f36 --- /dev/null +++ b/src/util/win32/msvc-compat.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_win32_msvc_compat_h__ +#define INCLUDE_win32_msvc_compat_h__ + +#if defined(_MSC_VER) + +typedef unsigned short mode_t; +typedef SSIZE_T ssize_t; + +#ifdef _WIN64 +# define SSIZE_MAX _I64_MAX +#else +# define SSIZE_MAX LONG_MAX +#endif + +#define strcasecmp(s1, s2) _stricmp(s1, s2) +#define strncasecmp(s1, s2, c) _strnicmp(s1, s2, c) + +#endif + +/* + * Offer GIT_LIBGIT2_CALL for our calling conventions (__cdecl, always). + * This is useful for providing callbacks to userspace code. + * + * Offer GIT_SYSTEM_CALL for the system calling conventions (__stdcall on + * Win32). Useful for providing callbacks to system libraries. + */ +#define GIT_LIBGIT2_CALL __cdecl +#define GIT_SYSTEM_CALL NTAPI + +#endif diff --git a/src/util/win32/path_w32.c b/src/util/win32/path_w32.c new file mode 100644 index 0000000..7a559e4 --- /dev/null +++ b/src/util/win32/path_w32.c @@ -0,0 +1,642 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "path_w32.h" + +#include "fs_path.h" +#include "utf-conv.h" +#include "posix.h" +#include "reparse.h" +#include "dir.h" + +#define PATH__NT_NAMESPACE L"\\\\?\\" +#define PATH__NT_NAMESPACE_LEN 4 + +#define PATH__ABSOLUTE_LEN 3 + +#define path__is_nt_namespace(p) \ + (((p)[0] == '\\' && (p)[1] == '\\' && (p)[2] == '?' && (p)[3] == '\\') || \ + ((p)[0] == '/' && (p)[1] == '/' && (p)[2] == '?' && (p)[3] == '/')) + +#define path__is_unc(p) \ + (((p)[0] == '\\' && (p)[1] == '\\') || ((p)[0] == '/' && (p)[1] == '/')) + +#define path__startswith_slash(p) \ + ((p)[0] == '\\' || (p)[0] == '/') + +GIT_INLINE(int) path__cwd(wchar_t *path, int size) +{ + int len; + + if ((len = GetCurrentDirectoryW(size, path)) == 0) { + errno = GetLastError() == ERROR_ACCESS_DENIED ? EACCES : ENOENT; + return -1; + } else if (len > size) { + errno = ENAMETOOLONG; + return -1; + } + + /* The Win32 APIs may return "\\?\" once you've used it first. + * But it may not. What a gloriously predictable API! + */ + if (wcsncmp(path, PATH__NT_NAMESPACE, PATH__NT_NAMESPACE_LEN)) + return len; + + len -= PATH__NT_NAMESPACE_LEN; + + memmove(path, path + PATH__NT_NAMESPACE_LEN, sizeof(wchar_t) * len); + return len; +} + +static wchar_t *path__skip_server(wchar_t *path) +{ + wchar_t *c; + + for (c = path; *c; c++) { + if (git_fs_path_is_dirsep(*c)) + return c + 1; + } + + return c; +} + +static wchar_t *path__skip_prefix(wchar_t *path) +{ + if (path__is_nt_namespace(path)) { + path += PATH__NT_NAMESPACE_LEN; + + if (wcsncmp(path, L"UNC\\", 4) == 0) + path = path__skip_server(path + 4); + else if (git_fs_path_is_absolute(path)) + path += PATH__ABSOLUTE_LEN; + } else if (git_fs_path_is_absolute(path)) { + path += PATH__ABSOLUTE_LEN; + } else if (path__is_unc(path)) { + path = path__skip_server(path + 2); + } + + return path; +} + +int git_win32_path_canonicalize(git_win32_path path) +{ + wchar_t *base, *from, *to, *next; + size_t len; + + base = to = path__skip_prefix(path); + + /* Unposixify if the prefix */ + for (from = path; from < to; from++) { + if (*from == L'/') + *from = L'\\'; + } + + while (*from) { + for (next = from; *next; ++next) { + if (*next == L'/') { + *next = L'\\'; + break; + } + + if (*next == L'\\') + break; + } + + len = next - from; + + if (len == 1 && from[0] == L'.') + /* do nothing with singleton dot */; + + else if (len == 2 && from[0] == L'.' && from[1] == L'.') { + if (to == base) { + /* no more path segments to strip, eat the "../" */ + if (*next == L'\\') + len++; + + base = to; + } else { + /* back up a path segment */ + while (to > base && to[-1] == L'\\') to--; + while (to > base && to[-1] != L'\\') to--; + } + } else { + if (*next == L'\\' && *from != L'\\') + len++; + + if (to != from) + memmove(to, from, sizeof(wchar_t) * len); + + to += len; + } + + from += len; + + while (*from == L'\\') from++; + } + + /* Strip trailing backslashes */ + while (to > base && to[-1] == L'\\') to--; + + *to = L'\0'; + + if ((to - path) > INT_MAX) { + SetLastError(ERROR_FILENAME_EXCED_RANGE); + return -1; + } + + return (int)(to - path); +} + +static int git_win32_path_join( + git_win32_path dest, + const wchar_t *one, + size_t one_len, + const wchar_t *two, + size_t two_len) +{ + size_t backslash = 0; + + if (one_len && two_len && one[one_len - 1] != L'\\') + backslash = 1; + + if (one_len + two_len + backslash > MAX_PATH) { + git_error_set(GIT_ERROR_INVALID, "path too long"); + return -1; + } + + memmove(dest, one, one_len * sizeof(wchar_t)); + + if (backslash) + dest[one_len] = L'\\'; + + memcpy(dest + one_len + backslash, two, two_len * sizeof(wchar_t)); + dest[one_len + backslash + two_len] = L'\0'; + + return 0; +} + +struct win32_path_iter { + wchar_t *env; + const wchar_t *current_dir; +}; + +static int win32_path_iter_init(struct win32_path_iter *iter) +{ + DWORD len = GetEnvironmentVariableW(L"PATH", NULL, 0); + + if (!len && GetLastError() == ERROR_ENVVAR_NOT_FOUND) { + iter->env = NULL; + iter->current_dir = NULL; + return 0; + } else if (!len) { + git_error_set(GIT_ERROR_OS, "could not load PATH"); + return -1; + } + + iter->env = git__malloc(len * sizeof(wchar_t)); + GIT_ERROR_CHECK_ALLOC(iter->env); + + len = GetEnvironmentVariableW(L"PATH", iter->env, len); + + if (len == 0) { + git_error_set(GIT_ERROR_OS, "could not load PATH"); + return -1; + } + + iter->current_dir = iter->env; + return 0; +} + +static int win32_path_iter_next( + const wchar_t **out, + size_t *out_len, + struct win32_path_iter *iter) +{ + const wchar_t *start; + wchar_t term; + size_t len = 0; + + if (!iter->current_dir || !*iter->current_dir) + return GIT_ITEROVER; + + term = (*iter->current_dir == L'"') ? *iter->current_dir++ : L';'; + start = iter->current_dir; + + while (*iter->current_dir && *iter->current_dir != term) { + iter->current_dir++; + len++; + } + + *out = start; + *out_len = len; + + if (term == L'"' && *iter->current_dir) + iter->current_dir++; + + while (*iter->current_dir == L';') + iter->current_dir++; + + return 0; +} + +static void win32_path_iter_dispose(struct win32_path_iter *iter) +{ + if (!iter) + return; + + git__free(iter->env); + iter->env = NULL; + iter->current_dir = NULL; +} + +int git_win32_path_find_executable(git_win32_path fullpath, wchar_t *exe) +{ + struct win32_path_iter path_iter; + const wchar_t *dir; + size_t dir_len, exe_len = wcslen(exe); + bool found = false; + + if (win32_path_iter_init(&path_iter) < 0) + return -1; + + while (win32_path_iter_next(&dir, &dir_len, &path_iter) != GIT_ITEROVER) { + if (git_win32_path_join(fullpath, dir, dir_len, exe, exe_len) < 0) + continue; + + if (_waccess(fullpath, 0) == 0) { + found = true; + break; + } + } + + win32_path_iter_dispose(&path_iter); + + if (found) + return 0; + + fullpath[0] = L'\0'; + return GIT_ENOTFOUND; +} + +static int win32_path_cwd(wchar_t *out, size_t len) +{ + int cwd_len; + + if (len > INT_MAX) { + errno = ENAMETOOLONG; + return -1; + } + + if ((cwd_len = path__cwd(out, (int)len)) < 0) + return -1; + + /* UNC paths */ + if (wcsncmp(L"\\\\", out, 2) == 0) { + /* Our buffer must be at least 5 characters larger than the + * current working directory: we swallow one of the leading + * '\'s, but we we add a 'UNC' specifier to the path, plus + * a trailing directory separator, plus a NUL. + */ + if (cwd_len > GIT_WIN_PATH_MAX - 4) { + errno = ENAMETOOLONG; + return -1; + } + + memmove(out+2, out, sizeof(wchar_t) * cwd_len); + out[0] = L'U'; + out[1] = L'N'; + out[2] = L'C'; + + cwd_len += 2; + } + + /* Our buffer must be at least 2 characters larger than the current + * working directory. (One character for the directory separator, + * one for the null. + */ + else if (cwd_len > GIT_WIN_PATH_MAX - 2) { + errno = ENAMETOOLONG; + return -1; + } + + return cwd_len; +} + +int git_win32_path_from_utf8(git_win32_path out, const char *src) +{ + wchar_t *dest = out; + + /* All win32 paths are in NT-prefixed format, beginning with "\\?\". */ + memcpy(dest, PATH__NT_NAMESPACE, sizeof(wchar_t) * PATH__NT_NAMESPACE_LEN); + dest += PATH__NT_NAMESPACE_LEN; + + /* See if this is an absolute path (beginning with a drive letter) */ + if (git_fs_path_is_absolute(src)) { + if (git_utf8_to_16(dest, GIT_WIN_PATH_MAX, src) < 0) + goto on_error; + } + /* File-prefixed NT-style paths beginning with \\?\ */ + else if (path__is_nt_namespace(src)) { + /* Skip the NT prefix, the destination already contains it */ + if (git_utf8_to_16(dest, GIT_WIN_PATH_MAX, src + PATH__NT_NAMESPACE_LEN) < 0) + goto on_error; + } + /* UNC paths */ + else if (path__is_unc(src)) { + memcpy(dest, L"UNC\\", sizeof(wchar_t) * 4); + dest += 4; + + /* Skip the leading "\\" */ + if (git_utf8_to_16(dest, GIT_WIN_PATH_MAX - 2, src + 2) < 0) + goto on_error; + } + /* Absolute paths omitting the drive letter */ + else if (path__startswith_slash(src)) { + if (path__cwd(dest, GIT_WIN_PATH_MAX) < 0) + goto on_error; + + if (!git_fs_path_is_absolute(dest)) { + errno = ENOENT; + goto on_error; + } + + /* Skip the drive letter specification ("C:") */ + if (git_utf8_to_16(dest + 2, GIT_WIN_PATH_MAX - 2, src) < 0) + goto on_error; + } + /* Relative paths */ + else { + int cwd_len; + + if ((cwd_len = win32_path_cwd(dest, GIT_WIN_PATH_MAX)) < 0) + goto on_error; + + dest[cwd_len++] = L'\\'; + + if (git_utf8_to_16(dest + cwd_len, GIT_WIN_PATH_MAX - cwd_len, src) < 0) + goto on_error; + } + + return git_win32_path_canonicalize(out); + +on_error: + /* set windows error code so we can use its error message */ + if (errno == ENAMETOOLONG) + SetLastError(ERROR_FILENAME_EXCED_RANGE); + + return -1; +} + +int git_win32_path_relative_from_utf8(git_win32_path out, const char *src) +{ + wchar_t *dest = out, *p; + int len; + + /* Handle absolute paths */ + if (git_fs_path_is_absolute(src) || + path__is_nt_namespace(src) || + path__is_unc(src) || + path__startswith_slash(src)) { + return git_win32_path_from_utf8(out, src); + } + + if ((len = git_utf8_to_16(dest, GIT_WIN_PATH_MAX, src)) < 0) + return -1; + + for (p = dest; p < (dest + len); p++) { + if (*p == L'/') + *p = L'\\'; + } + + return len; +} + +int git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src) +{ + char *out = dest; + int len; + + /* Strip NT namespacing "\\?\" */ + if (path__is_nt_namespace(src)) { + src += 4; + + /* "\\?\UNC\server\share" -> "\\server\share" */ + if (wcsncmp(src, L"UNC\\", 4) == 0) { + src += 4; + + memcpy(dest, "\\\\", 2); + out = dest + 2; + } + } + + if ((len = git_utf8_from_16(out, GIT_WIN_PATH_UTF8, src)) < 0) + return len; + + git_fs_path_mkposix(dest); + + return len; +} + +char *git_win32_path_8dot3_name(const char *path) +{ + git_win32_path longpath, shortpath; + wchar_t *start; + char *shortname; + int len, namelen = 1; + + if (git_win32_path_from_utf8(longpath, path) < 0) + return NULL; + + len = GetShortPathNameW(longpath, shortpath, GIT_WIN_PATH_UTF16); + + while (len && shortpath[len-1] == L'\\') + shortpath[--len] = L'\0'; + + if (len == 0 || len >= GIT_WIN_PATH_UTF16) + return NULL; + + for (start = shortpath + (len - 1); + start > shortpath && *(start-1) != '/' && *(start-1) != '\\'; + start--) + namelen++; + + /* We may not have actually been given a short name. But if we have, + * it will be in the ASCII byte range, so we don't need to worry about + * multi-byte sequences and can allocate naively. + */ + if (namelen > 12 || (shortname = git__malloc(namelen + 1)) == NULL) + return NULL; + + if ((len = git_utf8_from_16(shortname, namelen + 1, start)) < 0) + return NULL; + + return shortname; +} + +static bool path_is_volume(wchar_t *target, size_t target_len) +{ + return (target_len && wcsncmp(target, L"\\??\\Volume{", 11) == 0); +} + +/* On success, returns the length, in characters, of the path stored in dest. + * On failure, returns a negative value. */ +int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path) +{ + BYTE buf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + GIT_REPARSE_DATA_BUFFER *reparse_buf = (GIT_REPARSE_DATA_BUFFER *)buf; + HANDLE handle = NULL; + DWORD ioctl_ret; + wchar_t *target; + size_t target_len; + + int error = -1; + + handle = CreateFileW(path, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL); + + if (handle == INVALID_HANDLE_VALUE) { + errno = ENOENT; + return -1; + } + + if (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0, + reparse_buf, sizeof(buf), &ioctl_ret, NULL)) { + errno = EINVAL; + goto on_error; + } + + switch (reparse_buf->ReparseTag) { + case IO_REPARSE_TAG_SYMLINK: + target = reparse_buf->ReparseBuffer.SymbolicLink.PathBuffer + + (reparse_buf->ReparseBuffer.SymbolicLink.SubstituteNameOffset / sizeof(WCHAR)); + target_len = reparse_buf->ReparseBuffer.SymbolicLink.SubstituteNameLength / sizeof(WCHAR); + break; + case IO_REPARSE_TAG_MOUNT_POINT: + target = reparse_buf->ReparseBuffer.MountPoint.PathBuffer + + (reparse_buf->ReparseBuffer.MountPoint.SubstituteNameOffset / sizeof(WCHAR)); + target_len = reparse_buf->ReparseBuffer.MountPoint.SubstituteNameLength / sizeof(WCHAR); + break; + default: + errno = EINVAL; + goto on_error; + } + + if (path_is_volume(target, target_len)) { + /* This path is a reparse point that represents another volume mounted + * at this location, it is not a symbolic link our input was canonical. + */ + errno = EINVAL; + error = -1; + } else if (target_len) { + /* The path may need to have a namespace prefix removed. */ + target_len = git_win32_path_remove_namespace(target, target_len); + + /* Need one additional character in the target buffer + * for the terminating NULL. */ + if (GIT_WIN_PATH_UTF16 > target_len) { + wcscpy(dest, target); + error = (int)target_len; + } + } + +on_error: + CloseHandle(handle); + return error; +} + +/** + * Removes any trailing backslashes from a path, except in the case of a drive + * letter path (C:\, D:\, etc.). This function cannot fail. + * + * @param path The path which should be trimmed. + * @return The length of the modified string (<= the input length) + */ +size_t git_win32_path_trim_end(wchar_t *str, size_t len) +{ + while (1) { + if (!len || str[len - 1] != L'\\') + break; + + /* + * Don't trim backslashes from drive letter paths, which + * are 3 characters long and of the form C:\, D:\, etc. + */ + if (len == 3 && git_win32__isalpha(str[0]) && str[1] == ':') + break; + + len--; + } + + str[len] = L'\0'; + + return len; +} + +/** + * Removes any of the following namespace prefixes from a path, + * if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail. + * + * @param path The path which should be converted. + * @return The length of the modified string (<= the input length) + */ +size_t git_win32_path_remove_namespace(wchar_t *str, size_t len) +{ + static const wchar_t dosdevices_namespace[] = L"\\\?\?\\"; + static const wchar_t nt_namespace[] = L"\\\\?\\"; + static const wchar_t unc_namespace_remainder[] = L"UNC\\"; + static const wchar_t unc_prefix[] = L"\\\\"; + + const wchar_t *prefix = NULL, *remainder = NULL; + size_t prefix_len = 0, remainder_len = 0; + + /* "\??\" -- DOS Devices prefix */ + if (len >= CONST_STRLEN(dosdevices_namespace) && + !wcsncmp(str, dosdevices_namespace, CONST_STRLEN(dosdevices_namespace))) { + remainder = str + CONST_STRLEN(dosdevices_namespace); + remainder_len = len - CONST_STRLEN(dosdevices_namespace); + } + /* "\\?\" -- NT namespace prefix */ + else if (len >= CONST_STRLEN(nt_namespace) && + !wcsncmp(str, nt_namespace, CONST_STRLEN(nt_namespace))) { + remainder = str + CONST_STRLEN(nt_namespace); + remainder_len = len - CONST_STRLEN(nt_namespace); + } + + /* "\??\UNC\", "\\?\UNC\" -- UNC prefix */ + if (remainder_len >= CONST_STRLEN(unc_namespace_remainder) && + !wcsncmp(remainder, unc_namespace_remainder, CONST_STRLEN(unc_namespace_remainder))) { + + /* + * The proper Win32 path for a UNC share has "\\" at beginning of it + * and looks like "\\server\share\". So remove the + * UNC namespace and add a prefix of "\\" in its place. + */ + remainder += CONST_STRLEN(unc_namespace_remainder); + remainder_len -= CONST_STRLEN(unc_namespace_remainder); + + prefix = unc_prefix; + prefix_len = CONST_STRLEN(unc_prefix); + } + + /* + * Sanity check that the new string isn't longer than the old one. + * (This could only happen due to programmer error introducing a + * prefix longer than the namespace it replaces.) + */ + if (remainder && len >= remainder_len + prefix_len) { + if (prefix) + memmove(str, prefix, prefix_len * sizeof(wchar_t)); + + memmove(str + prefix_len, remainder, remainder_len * sizeof(wchar_t)); + + len = remainder_len + prefix_len; + str[len] = L'\0'; + } + + return git_win32_path_trim_end(str, len); +} diff --git a/src/util/win32/path_w32.h b/src/util/win32/path_w32.h new file mode 100644 index 0000000..b241d5c --- /dev/null +++ b/src/util/win32/path_w32.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_win32_path_w32_h__ +#define INCLUDE_win32_path_w32_h__ + +#include "git2_util.h" + +/** + * Create a Win32 path (in UCS-2 format) from a UTF-8 string. If the given + * path is relative, then it will be turned into an absolute path by having + * the current working directory prepended. + * + * @param dest The buffer to receive the wide string. + * @param src The UTF-8 string to convert. + * @return The length of the wide string, in characters (not counting the NULL terminator), or < 0 for failure + */ +extern int git_win32_path_from_utf8(git_win32_path dest, const char *src); + +/** + * Create a Win32 path (in UCS-2 format) from a UTF-8 string. If the given + * path is relative, then it will not be turned into an absolute path. + * + * @param dest The buffer to receive the wide string. + * @param src The UTF-8 string to convert. + * @return The length of the wide string, in characters (not counting the NULL terminator), or < 0 for failure + */ +extern int git_win32_path_relative_from_utf8(git_win32_path dest, const char *src); + +/** + * Canonicalize a Win32 UCS-2 path so that it is suitable for delivery to the + * Win32 APIs: remove multiple directory separators, squashing to a single one, + * strip trailing directory separators, ensure directory separators are all + * canonical (always backslashes, never forward slashes) and process any + * directory entries of '.' or '..'. + * + * Note that this is intended to be used on absolute Windows paths, those + * that start with `C:\`, `\\server\share`, `\\?\`, etc. + * + * This processes the buffer in place. + * + * @param path The buffer to process + * @return The new length of the buffer, in wchar_t's (not counting the NULL terminator) + */ +extern int git_win32_path_canonicalize(git_win32_path path); + +/** + * Create an internal format (posix-style) UTF-8 path from a Win32 UCS-2 path. + * + * @param dest The buffer to receive the UTF-8 string. + * @param src The wide string to convert. + * @return The length of the UTF-8 string, in bytes (not counting the NULL terminator), or < 0 for failure + */ +extern int git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src); + +/** + * Get the short name for the terminal path component in the given path. + * For example, given "C:\Foo\Bar\Asdf.txt", this will return the short name + * for the file "Asdf.txt". + * + * @param path The given path in UTF-8 + * @return The name of the shortname for the given path + */ +extern char *git_win32_path_8dot3_name(const char *path); + +extern int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path); + +/** + * Removes any trailing backslashes from a path, except in the case of a drive + * letter path (C:\, D:\, etc.). This function cannot fail. + * + * @param path The path which should be trimmed. + * @return The length of the modified string (<= the input length) + */ +size_t git_win32_path_trim_end(wchar_t *str, size_t len); + +/** + * Removes any of the following namespace prefixes from a path, + * if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail. + * + * @param path The path which should be converted. + * @return The length of the modified string (<= the input length) + */ +size_t git_win32_path_remove_namespace(wchar_t *str, size_t len); + +int git_win32_path_find_executable(git_win32_path fullpath, wchar_t* exe); + +#endif diff --git a/src/util/win32/posix.h b/src/util/win32/posix.h new file mode 100644 index 0000000..03fa2ac --- /dev/null +++ b/src/util/win32/posix.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_win32_posix_h__ +#define INCLUDE_win32_posix_h__ + +#include "git2_util.h" +#include "../posix.h" +#include "win32-compat.h" +#include "path_w32.h" +#include "utf-conv.h" +#include "dir.h" + +extern unsigned long git_win32__createfile_sharemode; +extern int git_win32__retries; + +typedef SOCKET GIT_SOCKET; + +#define p_lseek(f,n,w) _lseeki64(f, n, w) + +extern int p_fstat(int fd, struct stat *buf); +extern int p_lstat(const char *file_name, struct stat *buf); +extern int p_stat(const char *path, struct stat *buf); + +extern int p_utimes(const char *filename, const struct p_timeval times[2]); +extern int p_futimes(int fd, const struct p_timeval times[2]); + +extern int p_readlink(const char *path, char *buf, size_t bufsiz); +extern int p_symlink(const char *old, const char *new); +extern int p_link(const char *old, const char *new); +extern int p_unlink(const char *path); +extern int p_mkdir(const char *path, mode_t mode); +extern int p_fsync(int fd); +extern char *p_realpath(const char *orig_path, char *buffer); + +extern int p_recv(GIT_SOCKET socket, void *buffer, size_t length, int flags); +extern int p_send(GIT_SOCKET socket, const void *buffer, size_t length, int flags); +extern int p_inet_pton(int af, const char *src, void* dst); + +extern int p_vsnprintf(char *buffer, size_t count, const char *format, va_list argptr); +extern int p_snprintf(char *buffer, size_t count, const char *format, ...) GIT_FORMAT_PRINTF(3, 4); +extern int p_chdir(const char *path); +extern int p_chmod(const char *path, mode_t mode); +extern int p_rmdir(const char *path); +extern int p_access(const char *path, mode_t mode); +extern int p_ftruncate(int fd, off64_t size); + +/* p_lstat is almost but not quite POSIX correct. Specifically, the use of + * ENOTDIR is wrong, in that it does not mean precisely that a non-directory + * entry was encountered. Making it correct is potentially expensive, + * however, so this is a separate version of p_lstat to use when correct + * POSIX ENOTDIR semantics is required. + */ +extern int p_lstat_posixly(const char *filename, struct stat *buf); + +extern struct tm * p_localtime_r(const time_t *timer, struct tm *result); +extern struct tm * p_gmtime_r(const time_t *timer, struct tm *result); + +#endif diff --git a/src/util/win32/posix_w32.c b/src/util/win32/posix_w32.c new file mode 100644 index 0000000..3fec469 --- /dev/null +++ b/src/util/win32/posix_w32.c @@ -0,0 +1,1047 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2_util.h" + +#include "../posix.h" +#include "../futils.h" +#include "fs_path.h" +#include "path_w32.h" +#include "utf-conv.h" +#include "reparse.h" +#include +#include +#include +#include + +#ifndef FILE_NAME_NORMALIZED +# define FILE_NAME_NORMALIZED 0 +#endif + +#ifndef IO_REPARSE_TAG_SYMLINK +#define IO_REPARSE_TAG_SYMLINK (0xA000000CL) +#endif + +#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE +# define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x02 +#endif + +#ifndef SYMBOLIC_LINK_FLAG_DIRECTORY +# define SYMBOLIC_LINK_FLAG_DIRECTORY 0x01 +#endif + +/* Allowable mode bits on Win32. Using mode bits that are not supported on + * Win32 (eg S_IRWXU) is generally ignored, but Wine warns loudly about it + * so we simply remove them. + */ +#define WIN32_MODE_MASK (_S_IREAD | _S_IWRITE) + +unsigned long git_win32__createfile_sharemode = + FILE_SHARE_READ | FILE_SHARE_WRITE; +int git_win32__retries = 10; + +GIT_INLINE(void) set_errno(void) +{ + switch (GetLastError()) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_DRIVE: + case ERROR_NO_MORE_FILES: + case ERROR_BAD_NETPATH: + case ERROR_BAD_NET_NAME: + case ERROR_BAD_PATHNAME: + case ERROR_FILENAME_EXCED_RANGE: + errno = ENOENT; + break; + case ERROR_BAD_ENVIRONMENT: + errno = E2BIG; + break; + case ERROR_BAD_FORMAT: + case ERROR_INVALID_STARTING_CODESEG: + case ERROR_INVALID_STACKSEG: + case ERROR_INVALID_MODULETYPE: + case ERROR_INVALID_EXE_SIGNATURE: + case ERROR_EXE_MARKED_INVALID: + case ERROR_BAD_EXE_FORMAT: + case ERROR_ITERATED_DATA_EXCEEDS_64k: + case ERROR_INVALID_MINALLOCSIZE: + case ERROR_DYNLINK_FROM_INVALID_RING: + case ERROR_IOPL_NOT_ENABLED: + case ERROR_INVALID_SEGDPL: + case ERROR_AUTODATASEG_EXCEEDS_64k: + case ERROR_RING2SEG_MUST_BE_MOVABLE: + case ERROR_RELOC_CHAIN_XEEDS_SEGLIM: + case ERROR_INFLOOP_IN_RELOC_CHAIN: + errno = ENOEXEC; + break; + case ERROR_INVALID_HANDLE: + case ERROR_INVALID_TARGET_HANDLE: + case ERROR_DIRECT_ACCESS_HANDLE: + errno = EBADF; + break; + case ERROR_WAIT_NO_CHILDREN: + case ERROR_CHILD_NOT_COMPLETE: + errno = ECHILD; + break; + case ERROR_NO_PROC_SLOTS: + case ERROR_MAX_THRDS_REACHED: + case ERROR_NESTING_NOT_ALLOWED: + errno = EAGAIN; + break; + case ERROR_ARENA_TRASHED: + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_INVALID_BLOCK: + case ERROR_NOT_ENOUGH_QUOTA: + errno = ENOMEM; + break; + case ERROR_ACCESS_DENIED: + case ERROR_CURRENT_DIRECTORY: + case ERROR_WRITE_PROTECT: + case ERROR_BAD_UNIT: + case ERROR_NOT_READY: + case ERROR_BAD_COMMAND: + case ERROR_CRC: + case ERROR_BAD_LENGTH: + case ERROR_SEEK: + case ERROR_NOT_DOS_DISK: + case ERROR_SECTOR_NOT_FOUND: + case ERROR_OUT_OF_PAPER: + case ERROR_WRITE_FAULT: + case ERROR_READ_FAULT: + case ERROR_GEN_FAILURE: + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + case ERROR_WRONG_DISK: + case ERROR_SHARING_BUFFER_EXCEEDED: + case ERROR_NETWORK_ACCESS_DENIED: + case ERROR_CANNOT_MAKE: + case ERROR_FAIL_I24: + case ERROR_DRIVE_LOCKED: + case ERROR_SEEK_ON_DEVICE: + case ERROR_NOT_LOCKED: + case ERROR_LOCK_FAILED: + errno = EACCES; + break; + case ERROR_FILE_EXISTS: + case ERROR_ALREADY_EXISTS: + errno = EEXIST; + break; + case ERROR_NOT_SAME_DEVICE: + errno = EXDEV; + break; + case ERROR_INVALID_FUNCTION: + case ERROR_INVALID_ACCESS: + case ERROR_INVALID_DATA: + case ERROR_INVALID_PARAMETER: + case ERROR_NEGATIVE_SEEK: + errno = EINVAL; + break; + case ERROR_TOO_MANY_OPEN_FILES: + errno = EMFILE; + break; + case ERROR_DISK_FULL: + errno = ENOSPC; + break; + case ERROR_BROKEN_PIPE: + errno = EPIPE; + break; + case ERROR_DIR_NOT_EMPTY: + errno = ENOTEMPTY; + break; + default: + errno = EINVAL; + } +} + +GIT_INLINE(bool) last_error_retryable(void) +{ + int os_error = GetLastError(); + + return (os_error == ERROR_SHARING_VIOLATION || + os_error == ERROR_ACCESS_DENIED); +} + +#define do_with_retries(fn, remediation) \ + do { \ + int __retry, __ret; \ + for (__retry = git_win32__retries; __retry; __retry--) { \ + if ((__ret = (fn)) != GIT_RETRY) \ + return __ret; \ + if (__retry > 1 && (__ret = (remediation)) != 0) { \ + if (__ret == GIT_RETRY) \ + continue; \ + return __ret; \ + } \ + Sleep(5); \ + } \ + return -1; \ + } while (0) \ + +static int ensure_writable(wchar_t *path) +{ + DWORD attrs; + + if ((attrs = GetFileAttributesW(path)) == INVALID_FILE_ATTRIBUTES) + goto on_error; + + if ((attrs & FILE_ATTRIBUTE_READONLY) == 0) + return 0; + + if (!SetFileAttributesW(path, (attrs & ~FILE_ATTRIBUTE_READONLY))) + goto on_error; + + return GIT_RETRY; + +on_error: + set_errno(); + return -1; +} + +/** + * Truncate or extend file. + * + * We now take a "git_off_t" rather than "long" because + * files may be longer than 2Gb. + */ +int p_ftruncate(int fd, off64_t size) +{ + if (size < 0) { + errno = EINVAL; + return -1; + } + +#if !defined(__MINGW32__) || defined(MINGW_HAS_SECURE_API) + return ((_chsize_s(fd, size) == 0) ? 0 : -1); +#else + /* TODO MINGW32 Find a replacement for _chsize() that handles big files. */ + if (size > INT32_MAX) { + errno = EFBIG; + return -1; + } + return _chsize(fd, (long)size); +#endif +} + +int p_mkdir(const char *path, mode_t mode) +{ + git_win32_path buf; + + GIT_UNUSED(mode); + + if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + return _wmkdir(buf); +} + +int p_link(const char *old, const char *new) +{ + GIT_UNUSED(old); + GIT_UNUSED(new); + errno = ENOSYS; + return -1; +} + +GIT_INLINE(int) unlink_once(const wchar_t *path) +{ + DWORD error; + + if (DeleteFileW(path)) + return 0; + + if ((error = GetLastError()) == ERROR_ACCESS_DENIED) { + WIN32_FILE_ATTRIBUTE_DATA fdata; + if (!GetFileAttributesExW(path, GetFileExInfoStandard, &fdata) || + !(fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) || + !(fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + goto out; + + if (RemoveDirectoryW(path)) + return 0; + } + +out: + SetLastError(error); + + if (last_error_retryable()) + return GIT_RETRY; + + set_errno(); + return -1; +} + +int p_unlink(const char *path) +{ + git_win32_path wpath; + + if (git_win32_path_from_utf8(wpath, path) < 0) + return -1; + + do_with_retries(unlink_once(wpath), ensure_writable(wpath)); +} + +int p_fsync(int fd) +{ + HANDLE fh = (HANDLE)_get_osfhandle(fd); + + p_fsync__cnt++; + + if (fh == INVALID_HANDLE_VALUE) { + errno = EBADF; + return -1; + } + + if (!FlushFileBuffers(fh)) { + DWORD code = GetLastError(); + + if (code == ERROR_INVALID_HANDLE) + errno = EINVAL; + else + errno = EIO; + + return -1; + } + + return 0; +} + +#define WIN32_IS_WSEP(CH) ((CH) == L'/' || (CH) == L'\\') + +static int lstat_w( + wchar_t *path, + struct stat *buf, + bool posix_enotdir) +{ + WIN32_FILE_ATTRIBUTE_DATA fdata; + + if (GetFileAttributesExW(path, GetFileExInfoStandard, &fdata)) { + if (!buf) + return 0; + + return git_win32__file_attribute_to_stat(buf, &fdata, path); + } + + switch (GetLastError()) { + case ERROR_ACCESS_DENIED: + errno = EACCES; + break; + default: + errno = ENOENT; + break; + } + + /* To match POSIX behavior, set ENOTDIR when any of the folders in the + * file path is a regular file, otherwise set ENOENT. + */ + if (errno == ENOENT && posix_enotdir) { + size_t path_len = wcslen(path); + + /* scan up path until we find an existing item */ + while (1) { + DWORD attrs; + + /* remove last directory component */ + for (path_len--; path_len > 0 && !WIN32_IS_WSEP(path[path_len]); path_len--); + + if (path_len <= 0) + break; + + path[path_len] = L'\0'; + attrs = GetFileAttributesW(path); + + if (attrs != INVALID_FILE_ATTRIBUTES) { + if (!(attrs & FILE_ATTRIBUTE_DIRECTORY)) + errno = ENOTDIR; + break; + } + } + } + + return -1; +} + +static int do_lstat(const char *path, struct stat *buf, bool posixly_correct) +{ + git_win32_path path_w; + int len; + + if ((len = git_win32_path_from_utf8(path_w, path)) < 0) + return -1; + + git_win32_path_trim_end(path_w, len); + + return lstat_w(path_w, buf, posixly_correct); +} + +int p_lstat(const char *filename, struct stat *buf) +{ + return do_lstat(filename, buf, false); +} + +int p_lstat_posixly(const char *filename, struct stat *buf) +{ + return do_lstat(filename, buf, true); +} + +int p_readlink(const char *path, char *buf, size_t bufsiz) +{ + git_win32_path path_w, target_w; + git_win32_utf8_path target; + int len; + + /* readlink(2) does not NULL-terminate the string written + * to the target buffer. Furthermore, the target buffer need + * not be large enough to hold the entire result. A truncated + * result should be written in this case. Since this truncation + * could occur in the middle of the encoding of a code point, + * we need to buffer the result on the stack. */ + + if (git_win32_path_from_utf8(path_w, path) < 0 || + git_win32_path_readlink_w(target_w, path_w) < 0 || + (len = git_win32_path_to_utf8(target, target_w)) < 0) + return -1; + + bufsiz = min((size_t)len, bufsiz); + memcpy(buf, target, bufsiz); + + return (int)bufsiz; +} + +static bool target_is_dir(const char *target, const char *path) +{ + git_str resolved = GIT_STR_INIT; + git_win32_path resolved_w; + bool isdir = true; + + if (git_fs_path_is_absolute(target)) + git_win32_path_from_utf8(resolved_w, target); + else if (git_fs_path_dirname_r(&resolved, path) < 0 || + git_fs_path_apply_relative(&resolved, target) < 0 || + git_win32_path_from_utf8(resolved_w, resolved.ptr) < 0) + goto out; + + isdir = GetFileAttributesW(resolved_w) & FILE_ATTRIBUTE_DIRECTORY; + +out: + git_str_dispose(&resolved); + return isdir; +} + +int p_symlink(const char *target, const char *path) +{ + git_win32_path target_w, path_w; + DWORD dwFlags; + + /* + * Convert both target and path to Windows-style paths. Note that we do + * not want to use `git_win32_path_from_utf8` for converting the target, + * as that function will automatically pre-pend the current working + * directory in case the path is not absolute. As Git will instead use + * relative symlinks, this is not something we want. + */ + if (git_win32_path_from_utf8(path_w, path) < 0 || + git_win32_path_relative_from_utf8(target_w, target) < 0) + return -1; + + dwFlags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + if (target_is_dir(target, path)) + dwFlags |= SYMBOLIC_LINK_FLAG_DIRECTORY; + + if (!CreateSymbolicLinkW(path_w, target_w, dwFlags)) + return -1; + + return 0; +} + +struct open_opts { + DWORD access; + DWORD sharing; + SECURITY_ATTRIBUTES security; + DWORD creation_disposition; + DWORD attributes; + int osf_flags; +}; + +GIT_INLINE(void) open_opts_from_posix(struct open_opts *opts, int flags, mode_t mode) +{ + memset(opts, 0, sizeof(struct open_opts)); + + switch (flags & (O_WRONLY | O_RDWR)) { + case O_WRONLY: + opts->access = GENERIC_WRITE; + break; + case O_RDWR: + opts->access = GENERIC_READ | GENERIC_WRITE; + break; + default: + opts->access = GENERIC_READ; + break; + } + + opts->sharing = (DWORD)git_win32__createfile_sharemode; + + switch (flags & (O_CREAT | O_TRUNC | O_EXCL)) { + case O_CREAT | O_EXCL: + case O_CREAT | O_TRUNC | O_EXCL: + opts->creation_disposition = CREATE_NEW; + break; + case O_CREAT | O_TRUNC: + opts->creation_disposition = CREATE_ALWAYS; + break; + case O_TRUNC: + opts->creation_disposition = TRUNCATE_EXISTING; + break; + case O_CREAT: + opts->creation_disposition = OPEN_ALWAYS; + break; + default: + opts->creation_disposition = OPEN_EXISTING; + break; + } + + opts->attributes = ((flags & O_CREAT) && !(mode & S_IWRITE)) ? + FILE_ATTRIBUTE_READONLY : FILE_ATTRIBUTE_NORMAL; + opts->osf_flags = flags & (O_RDONLY | O_APPEND); + + opts->security.nLength = sizeof(SECURITY_ATTRIBUTES); + opts->security.lpSecurityDescriptor = NULL; + opts->security.bInheritHandle = 0; +} + +GIT_INLINE(int) open_once( + const wchar_t *path, + struct open_opts *opts) +{ + int fd; + + HANDLE handle = CreateFileW(path, opts->access, opts->sharing, + &opts->security, opts->creation_disposition, opts->attributes, 0); + + if (handle == INVALID_HANDLE_VALUE) { + if (last_error_retryable()) + return GIT_RETRY; + + set_errno(); + return -1; + } + + if ((fd = _open_osfhandle((intptr_t)handle, opts->osf_flags)) < 0) + CloseHandle(handle); + + return fd; +} + +int p_open(const char *path, int flags, ...) +{ + git_win32_path wpath; + mode_t mode = 0; + struct open_opts opts = {0}; + + #ifdef GIT_DEBUG_STRICT_OPEN + if (strstr(path, "//") != NULL) { + errno = EACCES; + return -1; + } + #endif + + if (git_win32_path_from_utf8(wpath, path) < 0) + return -1; + + if (flags & O_CREAT) { + va_list arg_list; + + va_start(arg_list, flags); + mode = (mode_t)va_arg(arg_list, int); + va_end(arg_list); + } + + open_opts_from_posix(&opts, flags, mode); + + do_with_retries( + open_once(wpath, &opts), + 0); +} + +int p_creat(const char *path, mode_t mode) +{ + return p_open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); +} + +int p_utimes(const char *path, const struct p_timeval times[2]) +{ + git_win32_path wpath; + int fd, error; + DWORD attrs_orig, attrs_new = 0; + struct open_opts opts = { 0 }; + + if (git_win32_path_from_utf8(wpath, path) < 0) + return -1; + + attrs_orig = GetFileAttributesW(wpath); + + if (attrs_orig & FILE_ATTRIBUTE_READONLY) { + attrs_new = attrs_orig & ~FILE_ATTRIBUTE_READONLY; + + if (!SetFileAttributesW(wpath, attrs_new)) { + git_error_set(GIT_ERROR_OS, "failed to set attributes"); + return -1; + } + } + + open_opts_from_posix(&opts, O_RDWR, 0); + + if ((fd = open_once(wpath, &opts)) < 0) { + error = -1; + goto done; + } + + error = p_futimes(fd, times); + close(fd); + +done: + if (attrs_orig != attrs_new) { + DWORD os_error = GetLastError(); + SetFileAttributesW(wpath, attrs_orig); + SetLastError(os_error); + } + + return error; +} + +int p_futimes(int fd, const struct p_timeval times[2]) +{ + HANDLE handle; + FILETIME atime = { 0 }, mtime = { 0 }; + + if (times == NULL) { + SYSTEMTIME st; + + GetSystemTime(&st); + SystemTimeToFileTime(&st, &atime); + SystemTimeToFileTime(&st, &mtime); + } + else { + git_win32__timeval_to_filetime(&atime, times[0]); + git_win32__timeval_to_filetime(&mtime, times[1]); + } + + if ((handle = (HANDLE)_get_osfhandle(fd)) == INVALID_HANDLE_VALUE) + return -1; + + if (SetFileTime(handle, NULL, &atime, &mtime) == 0) + return -1; + + return 0; +} + +int p_getcwd(char *buffer_out, size_t size) +{ + git_win32_path buf; + wchar_t *cwd = _wgetcwd(buf, GIT_WIN_PATH_UTF16); + + if (!cwd) + return -1; + + git_win32_path_remove_namespace(cwd, wcslen(cwd)); + + /* Convert the working directory back to UTF-8 */ + if (git_utf8_from_16(buffer_out, size, cwd) < 0) { + DWORD code = GetLastError(); + + if (code == ERROR_INSUFFICIENT_BUFFER) + errno = ERANGE; + else + errno = EINVAL; + + return -1; + } + + git_fs_path_mkposix(buffer_out); + return 0; +} + +static int getfinalpath_w( + git_win32_path dest, + const wchar_t *path) +{ + HANDLE hFile; + DWORD dwChars; + + /* Use FILE_FLAG_BACKUP_SEMANTICS so we can open a directory. Do not + * specify FILE_FLAG_OPEN_REPARSE_POINT; we want to open a handle to the + * target of the link. */ + hFile = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + + if (INVALID_HANDLE_VALUE == hFile) + return -1; + + /* Call GetFinalPathNameByHandle */ + dwChars = GetFinalPathNameByHandleW(hFile, dest, GIT_WIN_PATH_UTF16, FILE_NAME_NORMALIZED); + CloseHandle(hFile); + + if (!dwChars || dwChars >= GIT_WIN_PATH_UTF16) + return -1; + + /* The path may be delivered to us with a namespace prefix; remove */ + return (int)git_win32_path_remove_namespace(dest, dwChars); +} + +static int follow_and_lstat_link(git_win32_path path, struct stat *buf) +{ + git_win32_path target_w; + + if (getfinalpath_w(target_w, path) < 0) + return -1; + + return lstat_w(target_w, buf, false); +} + +int p_fstat(int fd, struct stat *buf) +{ + BY_HANDLE_FILE_INFORMATION fhInfo; + + HANDLE fh = (HANDLE)_get_osfhandle(fd); + + if (fh == INVALID_HANDLE_VALUE || + !GetFileInformationByHandle(fh, &fhInfo)) { + errno = EBADF; + return -1; + } + + git_win32__file_information_to_stat(buf, &fhInfo); + return 0; +} + +int p_stat(const char *path, struct stat *buf) +{ + git_win32_path path_w; + int len; + + if ((len = git_win32_path_from_utf8(path_w, path)) < 0 || + lstat_w(path_w, buf, false) < 0) + return -1; + + /* The item is a symbolic link or mount point. No need to iterate + * to follow multiple links; use GetFinalPathNameFromHandle. */ + if (S_ISLNK(buf->st_mode)) + return follow_and_lstat_link(path_w, buf); + + return 0; +} + +int p_chdir(const char *path) +{ + git_win32_path buf; + + if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + return _wchdir(buf); +} + +int p_chmod(const char *path, mode_t mode) +{ + git_win32_path buf; + + if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + return _wchmod(buf, mode); +} + +int p_rmdir(const char *path) +{ + git_win32_path buf; + int error; + + if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + error = _wrmdir(buf); + + if (error == -1) { + switch (GetLastError()) { + /* _wrmdir() is documented to return EACCES if "A program has an open + * handle to the directory." This sounds like what everybody else calls + * EBUSY. Let's convert appropriate error codes. + */ + case ERROR_SHARING_VIOLATION: + errno = EBUSY; + break; + + /* This error can be returned when trying to rmdir an extant file. */ + case ERROR_DIRECTORY: + errno = ENOTDIR; + break; + } + } + + return error; +} + +char *p_realpath(const char *orig_path, char *buffer) +{ + git_win32_path orig_path_w, buffer_w; + + if (git_win32_path_from_utf8(orig_path_w, orig_path) < 0) + return NULL; + + /* Note that if the path provided is a relative path, then the current directory + * is used to resolve the path -- which is a concurrency issue because the current + * directory is a process-wide variable. */ + if (!GetFullPathNameW(orig_path_w, GIT_WIN_PATH_UTF16, buffer_w, NULL)) { + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) + errno = ENAMETOOLONG; + else + errno = EINVAL; + + return NULL; + } + + /* The path must exist. */ + if (GetFileAttributesW(buffer_w) == INVALID_FILE_ATTRIBUTES) { + errno = ENOENT; + return NULL; + } + + if (!buffer && !(buffer = git__malloc(GIT_WIN_PATH_UTF8))) { + errno = ENOMEM; + return NULL; + } + + /* Convert the path to UTF-8. If the caller provided a buffer, then it + * is assumed to be GIT_WIN_PATH_UTF8 characters in size. If it isn't, + * then we may overflow. */ + if (git_win32_path_to_utf8(buffer, buffer_w) < 0) + return NULL; + + git_fs_path_mkposix(buffer); + + return buffer; +} + +int p_vsnprintf(char *buffer, size_t count, const char *format, va_list argptr) +{ +#if defined(_MSC_VER) + int len; + + if (count == 0) + return _vscprintf(format, argptr); + + #if _MSC_VER >= 1500 + len = _vsnprintf_s(buffer, count, _TRUNCATE, format, argptr); + #else + len = _vsnprintf(buffer, count, format, argptr); + #endif + + if (len < 0) + return _vscprintf(format, argptr); + + return len; +#else /* MinGW */ + return vsnprintf(buffer, count, format, argptr); +#endif +} + +int p_snprintf(char *buffer, size_t count, const char *format, ...) +{ + va_list va; + int r; + + va_start(va, format); + r = p_vsnprintf(buffer, count, format, va); + va_end(va); + + return r; +} + +int p_access(const char *path, mode_t mode) +{ + git_win32_path buf; + + if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + return _waccess(buf, mode & WIN32_MODE_MASK); +} + +GIT_INLINE(int) rename_once(const wchar_t *from, const wchar_t *to) +{ + if (MoveFileExW(from, to, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED)) + return 0; + + if (last_error_retryable()) + return GIT_RETRY; + + set_errno(); + return -1; +} + +int p_rename(const char *from, const char *to) +{ + git_win32_path wfrom, wto; + + if (git_win32_path_from_utf8(wfrom, from) < 0 || + git_win32_path_from_utf8(wto, to) < 0) + return -1; + + do_with_retries(rename_once(wfrom, wto), ensure_writable(wto)); +} + +int p_recv(GIT_SOCKET socket, void *buffer, size_t length, int flags) +{ + if ((size_t)((int)length) != length) + return -1; /* git_error_set will be done by caller */ + + return recv(socket, buffer, (int)length, flags); +} + +int p_send(GIT_SOCKET socket, const void *buffer, size_t length, int flags) +{ + if ((size_t)((int)length) != length) + return -1; /* git_error_set will be done by caller */ + + return send(socket, buffer, (int)length, flags); +} + +/** + * Borrowed from http://old.nabble.com/Porting-localtime_r-and-gmtime_r-td15282276.html + * On Win32, `gmtime_r` doesn't exist but `gmtime` is threadsafe, so we can use that + */ +struct tm * +p_localtime_r (const time_t *timer, struct tm *result) +{ + struct tm *local_result; + local_result = localtime (timer); + + if (local_result == NULL || result == NULL) + return NULL; + + memcpy (result, local_result, sizeof (struct tm)); + return result; +} +struct tm * +p_gmtime_r (const time_t *timer, struct tm *result) +{ + struct tm *local_result; + local_result = gmtime (timer); + + if (local_result == NULL || result == NULL) + return NULL; + + memcpy (result, local_result, sizeof (struct tm)); + return result; +} + +int p_inet_pton(int af, const char *src, void *dst) +{ + struct sockaddr_storage sin; + void *addr; + int sin_len = sizeof(struct sockaddr_storage), addr_len; + int error = 0; + + if (af == AF_INET) { + addr = &((struct sockaddr_in *)&sin)->sin_addr; + addr_len = sizeof(struct in_addr); + } else if (af == AF_INET6) { + addr = &((struct sockaddr_in6 *)&sin)->sin6_addr; + addr_len = sizeof(struct in6_addr); + } else { + errno = EAFNOSUPPORT; + return -1; + } + + if ((error = WSAStringToAddressA((LPSTR)src, af, NULL, (LPSOCKADDR)&sin, &sin_len)) == 0) { + memcpy(dst, addr, addr_len); + return 1; + } + + switch(WSAGetLastError()) { + case WSAEINVAL: + return 0; + case WSAEFAULT: + errno = ENOSPC; + return -1; + case WSA_NOT_ENOUGH_MEMORY: + errno = ENOMEM; + return -1; + } + + errno = EINVAL; + return -1; +} + +ssize_t p_pread(int fd, void *data, size_t size, off64_t offset) +{ + HANDLE fh; + DWORD rsize = 0; + OVERLAPPED ov = {0}; + LARGE_INTEGER pos = {0}; + off64_t final_offset = 0; + + /* Fail if the final offset would have overflowed to match POSIX semantics. */ + if (!git__is_ssizet(size) || git__add_int64_overflow(&final_offset, offset, (int64_t)size)) { + errno = EINVAL; + return -1; + } + + /* + * Truncate large writes to the maximum allowable size: the caller + * needs to always call this in a loop anyways. + */ + if (size > INT32_MAX) { + size = INT32_MAX; + } + + pos.QuadPart = offset; + ov.Offset = pos.LowPart; + ov.OffsetHigh = pos.HighPart; + fh = (HANDLE)_get_osfhandle(fd); + + if (ReadFile(fh, data, (DWORD)size, &rsize, &ov)) { + return (ssize_t)rsize; + } + + set_errno(); + return -1; +} + +ssize_t p_pwrite(int fd, const void *data, size_t size, off64_t offset) +{ + HANDLE fh; + DWORD wsize = 0; + OVERLAPPED ov = {0}; + LARGE_INTEGER pos = {0}; + off64_t final_offset = 0; + + /* Fail if the final offset would have overflowed to match POSIX semantics. */ + if (!git__is_ssizet(size) || git__add_int64_overflow(&final_offset, offset, (int64_t)size)) { + errno = EINVAL; + return -1; + } + + /* + * Truncate large writes to the maximum allowable size: the caller + * needs to always call this in a loop anyways. + */ + if (size > INT32_MAX) { + size = INT32_MAX; + } + + pos.QuadPart = offset; + ov.Offset = pos.LowPart; + ov.OffsetHigh = pos.HighPart; + fh = (HANDLE)_get_osfhandle(fd); + + if (WriteFile(fh, data, (DWORD)size, &wsize, &ov)) { + return (ssize_t)wsize; + } + + set_errno(); + return -1; +} diff --git a/src/util/win32/precompiled.c b/src/util/win32/precompiled.c new file mode 100644 index 0000000..5f656a4 --- /dev/null +++ b/src/util/win32/precompiled.c @@ -0,0 +1 @@ +#include "precompiled.h" diff --git a/src/util/win32/precompiled.h b/src/util/win32/precompiled.h new file mode 100644 index 0000000..1163c3d --- /dev/null +++ b/src/util/win32/precompiled.h @@ -0,0 +1,21 @@ +#include "git2_util.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#ifdef GIT_THREADS + #include "win32/thread.h" +#endif + +#include "git2.h" diff --git a/src/util/win32/reparse.h b/src/util/win32/reparse.h new file mode 100644 index 0000000..2331231 --- /dev/null +++ b/src/util/win32/reparse.h @@ -0,0 +1,57 @@ +/* +* Copyright (C) the libgit2 contributors. All rights reserved. +* +* This file is part of libgit2, distributed under the GNU GPL v2 with +* a Linking Exception. For full terms see the included COPYING file. +*/ + +#ifndef INCLUDE_win32_reparse_h__ +#define INCLUDE_win32_reparse_h__ + +/* This structure is defined on MSDN at +* http://msdn.microsoft.com/en-us/library/windows/hardware/ff552012(v=vs.85).aspx +* +* It was formerly included in the Windows 2000 SDK and remains defined in +* MinGW, so we must define it with a silly name to avoid conflicting. +*/ +typedef struct _GIT_REPARSE_DATA_BUFFER { + ULONG ReparseTag; + USHORT ReparseDataLength; + USHORT Reserved; + union { + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + ULONG Flags; + WCHAR PathBuffer[1]; + } SymbolicLink; + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + WCHAR PathBuffer[1]; + } MountPoint; + struct { + UCHAR DataBuffer[1]; + } Generic; + } ReparseBuffer; +} GIT_REPARSE_DATA_BUFFER; + +#define REPARSE_DATA_HEADER_SIZE 8 +#define REPARSE_DATA_MOUNTPOINT_HEADER_SIZE 8 +#define REPARSE_DATA_UNION_SIZE 12 + +/* Missing in MinGW */ +#ifndef FSCTL_GET_REPARSE_POINT +# define FSCTL_GET_REPARSE_POINT 0x000900a8 +#endif + +/* Missing in MinGW */ +#ifndef FSCTL_SET_REPARSE_POINT +# define FSCTL_SET_REPARSE_POINT 0x000900a4 +#endif + +#endif diff --git a/src/util/win32/thread.c b/src/util/win32/thread.c new file mode 100644 index 0000000..f5cacd3 --- /dev/null +++ b/src/util/win32/thread.c @@ -0,0 +1,262 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "thread.h" +#include "runtime.h" + +#define CLEAN_THREAD_EXIT 0x6F012842 + +typedef void (WINAPI *win32_srwlock_fn)(GIT_SRWLOCK *); + +static win32_srwlock_fn win32_srwlock_initialize; +static win32_srwlock_fn win32_srwlock_acquire_shared; +static win32_srwlock_fn win32_srwlock_release_shared; +static win32_srwlock_fn win32_srwlock_acquire_exclusive; +static win32_srwlock_fn win32_srwlock_release_exclusive; + +static DWORD fls_index; + +/* The thread procedure stub used to invoke the caller's procedure + * and capture the return value for later collection. Windows will + * only hold a DWORD, but we need to be able to store an entire + * void pointer. This requires the indirection. */ +static DWORD WINAPI git_win32__threadproc(LPVOID lpParameter) +{ + git_thread *thread = lpParameter; + + /* Set the current thread for `git_thread_exit` */ + FlsSetValue(fls_index, thread); + + thread->result = thread->proc(thread->param); + + return CLEAN_THREAD_EXIT; +} + +static void git_threads_global_shutdown(void) +{ + FlsFree(fls_index); +} + +int git_threads_global_init(void) +{ + HMODULE hModule = GetModuleHandleW(L"kernel32"); + + if (hModule) { + win32_srwlock_initialize = (win32_srwlock_fn)(void *) + GetProcAddress(hModule, "InitializeSRWLock"); + win32_srwlock_acquire_shared = (win32_srwlock_fn)(void *) + GetProcAddress(hModule, "AcquireSRWLockShared"); + win32_srwlock_release_shared = (win32_srwlock_fn)(void *) + GetProcAddress(hModule, "ReleaseSRWLockShared"); + win32_srwlock_acquire_exclusive = (win32_srwlock_fn)(void *) + GetProcAddress(hModule, "AcquireSRWLockExclusive"); + win32_srwlock_release_exclusive = (win32_srwlock_fn)(void *) + GetProcAddress(hModule, "ReleaseSRWLockExclusive"); + } + + if ((fls_index = FlsAlloc(NULL)) == FLS_OUT_OF_INDEXES) + return -1; + + return git_runtime_shutdown_register(git_threads_global_shutdown); +} + +int git_thread_create( + git_thread *GIT_RESTRICT thread, + void *(*start_routine)(void*), + void *GIT_RESTRICT arg) +{ + thread->result = NULL; + thread->param = arg; + thread->proc = start_routine; + thread->thread = CreateThread( + NULL, 0, git_win32__threadproc, thread, 0, NULL); + + return thread->thread ? 0 : -1; +} + +int git_thread_join( + git_thread *thread, + void **value_ptr) +{ + DWORD exit; + + if (WaitForSingleObject(thread->thread, INFINITE) != WAIT_OBJECT_0) + return -1; + + if (!GetExitCodeThread(thread->thread, &exit)) { + CloseHandle(thread->thread); + return -1; + } + + /* Check for the thread having exited uncleanly. If exit was unclean, + * then we don't have a return value to give back to the caller. */ + GIT_ASSERT(exit == CLEAN_THREAD_EXIT); + + if (value_ptr) + *value_ptr = thread->result; + + CloseHandle(thread->thread); + return 0; +} + +void git_thread_exit(void *value) +{ + git_thread *thread = FlsGetValue(fls_index); + + if (thread) + thread->result = value; + + ExitThread(CLEAN_THREAD_EXIT); +} + +size_t git_thread_currentid(void) +{ + return GetCurrentThreadId(); +} + +int git_mutex_init(git_mutex *GIT_RESTRICT mutex) +{ + InitializeCriticalSection(mutex); + return 0; +} + +int git_mutex_free(git_mutex *mutex) +{ + DeleteCriticalSection(mutex); + return 0; +} + +int git_mutex_lock(git_mutex *mutex) +{ + EnterCriticalSection(mutex); + return 0; +} + +int git_mutex_unlock(git_mutex *mutex) +{ + LeaveCriticalSection(mutex); + return 0; +} + +int git_cond_init(git_cond *cond) +{ + /* This is an auto-reset event. */ + *cond = CreateEventW(NULL, FALSE, FALSE, NULL); + GIT_ASSERT(*cond); + + /* If we can't create the event, claim that the reason was out-of-memory. + * The actual reason can be fetched with GetLastError(). */ + return *cond ? 0 : ENOMEM; +} + +int git_cond_free(git_cond *cond) +{ + BOOL closed; + + if (!cond) + return EINVAL; + + closed = CloseHandle(*cond); + GIT_ASSERT(closed); + GIT_UNUSED(closed); + + *cond = NULL; + return 0; +} + +int git_cond_wait(git_cond *cond, git_mutex *mutex) +{ + int error; + DWORD wait_result; + + if (!cond || !mutex) + return EINVAL; + + /* The caller must be holding the mutex. */ + error = git_mutex_unlock(mutex); + + if (error) + return error; + + wait_result = WaitForSingleObject(*cond, INFINITE); + GIT_ASSERT(WAIT_OBJECT_0 == wait_result); + GIT_UNUSED(wait_result); + + return git_mutex_lock(mutex); +} + +int git_cond_signal(git_cond *cond) +{ + BOOL signaled; + + if (!cond) + return EINVAL; + + signaled = SetEvent(*cond); + GIT_ASSERT(signaled); + GIT_UNUSED(signaled); + + return 0; +} + +int git_rwlock_init(git_rwlock *GIT_RESTRICT lock) +{ + if (win32_srwlock_initialize) + win32_srwlock_initialize(&lock->native.srwl); + else + InitializeCriticalSection(&lock->native.csec); + + return 0; +} + +int git_rwlock_rdlock(git_rwlock *lock) +{ + if (win32_srwlock_acquire_shared) + win32_srwlock_acquire_shared(&lock->native.srwl); + else + EnterCriticalSection(&lock->native.csec); + + return 0; +} + +int git_rwlock_rdunlock(git_rwlock *lock) +{ + if (win32_srwlock_release_shared) + win32_srwlock_release_shared(&lock->native.srwl); + else + LeaveCriticalSection(&lock->native.csec); + + return 0; +} + +int git_rwlock_wrlock(git_rwlock *lock) +{ + if (win32_srwlock_acquire_exclusive) + win32_srwlock_acquire_exclusive(&lock->native.srwl); + else + EnterCriticalSection(&lock->native.csec); + + return 0; +} + +int git_rwlock_wrunlock(git_rwlock *lock) +{ + if (win32_srwlock_release_exclusive) + win32_srwlock_release_exclusive(&lock->native.srwl); + else + LeaveCriticalSection(&lock->native.csec); + + return 0; +} + +int git_rwlock_free(git_rwlock *lock) +{ + if (!win32_srwlock_initialize) + DeleteCriticalSection(&lock->native.csec); + git__memzero(lock, sizeof(*lock)); + return 0; +} diff --git a/src/util/win32/thread.h b/src/util/win32/thread.h new file mode 100644 index 0000000..184762e --- /dev/null +++ b/src/util/win32/thread.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_win32_thread_h__ +#define INCLUDE_win32_thread_h__ + +#include "git2_util.h" + +#if defined (_MSC_VER) +# define GIT_RESTRICT __restrict +#else +# define GIT_RESTRICT __restrict__ +#endif + +typedef struct { + HANDLE thread; + void *(*proc)(void *); + void *param; + void *result; +} git_thread; + +typedef CRITICAL_SECTION git_mutex; +typedef HANDLE git_cond; + +typedef struct { void *Ptr; } GIT_SRWLOCK; + +typedef struct { + union { + GIT_SRWLOCK srwl; + CRITICAL_SECTION csec; + } native; +} git_rwlock; + +int git_threads_global_init(void); + +int git_thread_create(git_thread *GIT_RESTRICT, + void *(*) (void *), + void *GIT_RESTRICT); +int git_thread_join(git_thread *, void **); +size_t git_thread_currentid(void); +void git_thread_exit(void *); + +int git_mutex_init(git_mutex *GIT_RESTRICT mutex); +int git_mutex_free(git_mutex *); +int git_mutex_lock(git_mutex *); +int git_mutex_unlock(git_mutex *); + +int git_cond_init(git_cond *); +int git_cond_free(git_cond *); +int git_cond_wait(git_cond *, git_mutex *); +int git_cond_signal(git_cond *); + +int git_rwlock_init(git_rwlock *GIT_RESTRICT lock); +int git_rwlock_rdlock(git_rwlock *); +int git_rwlock_rdunlock(git_rwlock *); +int git_rwlock_wrlock(git_rwlock *); +int git_rwlock_wrunlock(git_rwlock *); +int git_rwlock_free(git_rwlock *); + +#endif diff --git a/src/util/win32/utf-conv.c b/src/util/win32/utf-conv.c new file mode 100644 index 0000000..ad35c0c --- /dev/null +++ b/src/util/win32/utf-conv.c @@ -0,0 +1,144 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "utf-conv.h" + +GIT_INLINE(void) git__set_errno(void) +{ + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) + errno = ENAMETOOLONG; + else + errno = EINVAL; +} + +int git_utf8_to_16(wchar_t *dest, size_t dest_size, const char *src) +{ + /* Length of -1 indicates NULL termination of the input string. */ + return git_utf8_to_16_with_len(dest, dest_size, src, -1); +} + +int git_utf8_to_16_with_len( + wchar_t *dest, + size_t _dest_size, + const char *src, + int src_len) +{ + int dest_size = (int)min(_dest_size, INT_MAX); + int len; + + /* + * Subtract 1 from the result to turn 0 into -1 (an error code) and + * to not count the NULL terminator as part of the string's length. + * MultiByteToWideChar never returns int's minvalue, so underflow + * is not possible. + */ + len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + src, src_len, dest, dest_size) - 1; + + if (len < 0) + git__set_errno(); + + return len; +} + +int git_utf8_from_16(char *dest, size_t dest_size, const wchar_t *src) +{ + /* Length of -1 indicates NULL termination of the input string. */ + return git_utf8_from_16_with_len(dest, dest_size, src, -1); +} + +int git_utf8_from_16_with_len( + char *dest, + size_t _dest_size, + const wchar_t *src, + int src_len) +{ + int dest_size = (int)min(_dest_size, INT_MAX); + int len; + + /* + * Subtract 1 from the result to turn 0 into -1 (an error code) and + * to not count the NULL terminator as part of the string's length. + * WideCharToMultiByte never returns int's minvalue, so underflow + * is not possible. + */ + len = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + src, src_len, dest, dest_size, NULL, NULL) - 1; + + if (len < 0) + git__set_errno(); + + return len; +} + +int git_utf8_to_16_alloc(wchar_t **dest, const char *src) +{ + /* Length of -1 indicates NULL termination of the input string. */ + return git_utf8_to_16_alloc_with_len(dest, src, -1); +} + +int git_utf8_to_16_alloc_with_len(wchar_t **dest, const char *src, int src_len) +{ + int utf16_size; + + *dest = NULL; + + utf16_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + src, src_len, NULL, 0); + + if (!utf16_size) { + git__set_errno(); + return -1; + } + + *dest = git__mallocarray(utf16_size, sizeof(wchar_t)); + GIT_ERROR_CHECK_ALLOC(*dest); + + utf16_size = git_utf8_to_16_with_len(*dest, (size_t)utf16_size, + src, src_len); + + if (utf16_size < 0) { + git__free(*dest); + *dest = NULL; + } + + return utf16_size; +} + +int git_utf8_from_16_alloc(char **dest, const wchar_t *src) +{ + /* Length of -1 indicates NULL termination of the input string. */ + return git_utf8_from_16_alloc_with_len(dest, src, -1); +} + +int git_utf8_from_16_alloc_with_len(char **dest, const wchar_t *src, int src_len) +{ + int utf8_size; + + *dest = NULL; + + utf8_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + src, src_len, NULL, 0, NULL, NULL); + + if (!utf8_size) { + git__set_errno(); + return -1; + } + + *dest = git__malloc(utf8_size); + GIT_ERROR_CHECK_ALLOC(*dest); + + utf8_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + src, src_len, *dest, utf8_size, NULL, NULL); + + if (utf8_size < 0) { + git__free(*dest); + *dest = NULL; + } + + return utf8_size; +} diff --git a/src/util/win32/utf-conv.h b/src/util/win32/utf-conv.h new file mode 100644 index 0000000..301f5a6 --- /dev/null +++ b/src/util/win32/utf-conv.h @@ -0,0 +1,127 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_win32_utf_conv_h__ +#define INCLUDE_win32_utf_conv_h__ + +#include "git2_util.h" + +#include + +#ifndef WC_ERR_INVALID_CHARS +# define WC_ERR_INVALID_CHARS 0x80 +#endif + +/** + * Converts a NUL-terminated UTF-8 string to wide characters. This is a + * convenience function for `git_utf8_to_16_with_len`. + * + * @param dest The buffer to receive the wide string. + * @param dest_size The size of the buffer, in characters. + * @param src The UTF-8 string to convert. + * @return The length of the wide string, in characters + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_to_16(wchar_t *dest, size_t dest_size, const char *src); + +/** + * Converts a UTF-8 string to wide characters. + * + * @param dest The buffer to receive the wide string. + * @param dest_size The size of the buffer, in characters. + * @param src The UTF-8 string to convert. + * @param src_len The length of the string to convert. + * @return The length of the wide string, in characters + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_to_16_with_len( + wchar_t *dest, + size_t dest_size, + const char *src, + int src_len); + +/** + * Converts a NUL-terminated wide string to UTF-8. This is a convenience + * function for `git_utf8_from_16_with_len`. + * + * @param dest The buffer to receive the UTF-8 string. + * @param dest_size The size of the buffer, in bytes. + * @param src The wide string to convert. + * @param src_len The length of the string to convert. + * @return The length of the UTF-8 string, in bytes + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_from_16(char *dest, size_t dest_size, const wchar_t *src); + +/** + * Converts a wide string to UTF-8. + * + * @param dest The buffer to receive the UTF-8 string. + * @param dest_size The size of the buffer, in bytes. + * @param src The wide string to convert. + * @param src_len The length of the string to convert. + * @return The length of the UTF-8 string, in bytes + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_from_16_with_len(char *dest, size_t dest_size, const wchar_t *src, int src_len); + +/** + * Converts a UTF-8 string to wide characters. Memory is allocated to hold + * the converted string. The caller is responsible for freeing the string + * with git__free. + * + * @param dest Receives a pointer to the wide string. + * @param src The UTF-8 string to convert. + * @return The length of the wide string, in characters + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_to_16_alloc(wchar_t **dest, const char *src); + +/** + * Converts a UTF-8 string to wide characters. Memory is allocated to hold + * the converted string. The caller is responsible for freeing the string + * with git__free. + * + * @param dest Receives a pointer to the wide string. + * @param src The UTF-8 string to convert. + * @param src_len The length of the string. + * @return The length of the wide string, in characters + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_to_16_alloc_with_len( + wchar_t **dest, + const char *src, + int src_len); + +/** + * Converts a wide string to UTF-8. Memory is allocated to hold the + * converted string. The caller is responsible for freeing the string + * with git__free. + * + * @param dest Receives a pointer to the UTF-8 string. + * @param src The wide string to convert. + * @return The length of the UTF-8 string, in bytes + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_from_16_alloc(char **dest, const wchar_t *src); + +/** + * Converts a wide string to UTF-8. Memory is allocated to hold the + * converted string. The caller is responsible for freeing the string + * with git__free. + * + * @param dest Receives a pointer to the UTF-8 string. + * @param src The wide string to convert. + * @param src_len The length of the wide string. + * @return The length of the UTF-8 string, in bytes + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_from_16_alloc_with_len( + char **dest, + const wchar_t *src, + int src_len); + +#endif diff --git a/src/util/win32/version.h b/src/util/win32/version.h new file mode 100644 index 0000000..7966769 --- /dev/null +++ b/src/util/win32/version.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_win32_version_h__ +#define INCLUDE_win32_version_h__ + +#include + +GIT_INLINE(int) git_has_win32_version(int major, int minor, int service_pack) +{ + OSVERSIONINFOEX version_test = {0}; + DWORD version_test_mask; + DWORDLONG version_condition_mask = 0; + + version_test.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); + version_test.dwMajorVersion = major; + version_test.dwMinorVersion = minor; + version_test.wServicePackMajor = (WORD)service_pack; + version_test.wServicePackMinor = 0; + + version_test_mask = (VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR | VER_SERVICEPACKMINOR); + + VER_SET_CONDITION(version_condition_mask, VER_MAJORVERSION, VER_GREATER_EQUAL); + VER_SET_CONDITION(version_condition_mask, VER_MINORVERSION, VER_GREATER_EQUAL); + VER_SET_CONDITION(version_condition_mask, VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); + VER_SET_CONDITION(version_condition_mask, VER_SERVICEPACKMINOR, VER_GREATER_EQUAL); + + if (!VerifyVersionInfo(&version_test, version_test_mask, version_condition_mask)) + return 0; + + return 1; +} + +#endif diff --git a/src/util/win32/w32_buffer.c b/src/util/win32/w32_buffer.c new file mode 100644 index 0000000..6fee820 --- /dev/null +++ b/src/util/win32/w32_buffer.c @@ -0,0 +1,57 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "w32_buffer.h" + +#include "utf-conv.h" + +GIT_INLINE(int) handle_wc_error(void) +{ + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) + errno = ENAMETOOLONG; + else + errno = EINVAL; + + return -1; +} + +int git_str_put_w(git_str *buf, const wchar_t *string_w, size_t len_w) +{ + int utf8_len, utf8_write_len; + size_t new_size; + + if (!len_w) { + return 0; + } else if (len_w > INT_MAX) { + git_error_set_oom(); + return -1; + } + + GIT_ASSERT(string_w); + + /* Measure the string necessary for conversion */ + if ((utf8_len = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string_w, (int)len_w, NULL, 0, NULL, NULL)) == 0) + return 0; + + GIT_ASSERT(utf8_len > 0); + + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, buf->size, (size_t)utf8_len); + GIT_ERROR_CHECK_ALLOC_ADD(&new_size, new_size, 1); + + if (git_str_grow(buf, new_size) < 0) + return -1; + + if ((utf8_write_len = WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, string_w, (int)len_w, &buf->ptr[buf->size], utf8_len, NULL, NULL)) == 0) + return handle_wc_error(); + + GIT_ASSERT(utf8_write_len == utf8_len); + + buf->size += utf8_write_len; + buf->ptr[buf->size] = '\0'; + return 0; +} diff --git a/src/util/win32/w32_buffer.h b/src/util/win32/w32_buffer.h new file mode 100644 index 0000000..68ea960 --- /dev/null +++ b/src/util/win32/w32_buffer.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_win32_w32_buffer_h__ +#define INCLUDE_win32_w32_buffer_h__ + +#include "git2_util.h" +#include "str.h" + +/** + * Convert a wide character string to UTF-8 and append the results to the + * buffer. + */ +int git_str_put_w(git_str *buf, const wchar_t *string_w, size_t len_w); + +#endif diff --git a/src/util/win32/w32_common.h b/src/util/win32/w32_common.h new file mode 100644 index 0000000..c20b3e8 --- /dev/null +++ b/src/util/win32/w32_common.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_win32_w32_common_h__ +#define INCLUDE_win32_w32_common_h__ + +#include + +/* + * 4096 is the max allowed Git path. `MAX_PATH` (260) is the typical max allowed + * Windows path length, however win32 Unicode APIs generally allow up to 32,767 + * if prefixed with "\\?\" (i.e. converted to an NT-style name). + */ +#define GIT_WIN_PATH_MAX GIT_PATH_MAX + +/* + * Provides a large enough buffer to support Windows Git paths: + * GIT_WIN_PATH_MAX is 4096, corresponding to a maximum path length of 4095 + * characters plus a NULL terminator. Prefixing with "\\?\" adds 4 characters, + * but if the original was a UNC path, then we turn "\\server\share" into + * "\\?\UNC\server\share". So we replace the first two characters with + * 8 characters, a net gain of 6, so the maximum length is GIT_WIN_PATH_MAX+6. + */ +#define GIT_WIN_PATH_UTF16 GIT_WIN_PATH_MAX+6 + +/* Maximum size of a UTF-8 Win32 Git path. We remove the "\\?\" or "\\?\UNC\" + * prefixes for presentation, bringing us back to 4095 (non-NULL) + * characters. UTF-8 does have 4-byte sequences, but they are encoded in + * UTF-16 using surrogate pairs, which takes up the space of two characters. + * Two characters in the range U+0800 -> U+FFFF take up more space in UTF-8 + * (6 bytes) than one surrogate pair (4 bytes). + */ +#define GIT_WIN_PATH_UTF8 ((GIT_WIN_PATH_MAX - 1) * 3 + 1) + +/* + * The length of a Windows "shortname", for 8.3 compatibility. + */ +#define GIT_WIN_PATH_SHORTNAME 13 + +/* Win32 path types */ +typedef wchar_t git_win32_path[GIT_WIN_PATH_UTF16]; +typedef char git_win32_utf8_path[GIT_WIN_PATH_UTF8]; + +#endif diff --git a/src/util/win32/w32_leakcheck.c b/src/util/win32/w32_leakcheck.c new file mode 100644 index 0000000..0f095de --- /dev/null +++ b/src/util/win32/w32_leakcheck.c @@ -0,0 +1,581 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "w32_leakcheck.h" + +#if defined(GIT_WIN32_LEAKCHECK) + +#include "Windows.h" +#include "Dbghelp.h" +#include "win32/posix.h" +#include "hash.h" +#include "runtime.h" + +/* Stack frames (for stack tracing, below) */ + +static bool g_win32_stack_initialized = false; +static HANDLE g_win32_stack_process = INVALID_HANDLE_VALUE; +static git_win32_leakcheck_stack_aux_cb_alloc g_aux_cb_alloc = NULL; +static git_win32_leakcheck_stack_aux_cb_lookup g_aux_cb_lookup = NULL; + +int git_win32_leakcheck_stack_set_aux_cb( + git_win32_leakcheck_stack_aux_cb_alloc cb_alloc, + git_win32_leakcheck_stack_aux_cb_lookup cb_lookup) +{ + g_aux_cb_alloc = cb_alloc; + g_aux_cb_lookup = cb_lookup; + + return 0; +} + +/** + * Load symbol table data. This should be done in the primary + * thread at startup (under a lock if there are other threads + * active). + */ +void git_win32_leakcheck_stack_init(void) +{ + if (!g_win32_stack_initialized) { + g_win32_stack_process = GetCurrentProcess(); + SymSetOptions(SYMOPT_LOAD_LINES); + SymInitialize(g_win32_stack_process, NULL, TRUE); + g_win32_stack_initialized = true; + } +} + +/** + * Cleanup symbol table data. This should be done in the + * primary thead at shutdown (under a lock if there are other + * threads active). + */ +void git_win32_leakcheck_stack_cleanup(void) +{ + if (g_win32_stack_initialized) { + SymCleanup(g_win32_stack_process); + g_win32_stack_process = INVALID_HANDLE_VALUE; + g_win32_stack_initialized = false; + } +} + +int git_win32_leakcheck_stack_capture(git_win32_leakcheck_stack_raw_data *pdata, int skip) +{ + if (!g_win32_stack_initialized) { + git_error_set(GIT_ERROR_INVALID, "git_win32_stack not initialized."); + return GIT_ERROR; + } + + memset(pdata, 0, sizeof(*pdata)); + pdata->nr_frames = RtlCaptureStackBackTrace( + skip+1, GIT_WIN32_LEAKCHECK_STACK_MAX_FRAMES, pdata->frames, NULL); + + /* If an "aux" data provider was registered, ask it to capture + * whatever data it needs and give us an "aux_id" to it so that + * we can refer to it later when reporting. + */ + if (g_aux_cb_alloc) + (g_aux_cb_alloc)(&pdata->aux_id); + + return 0; +} + +int git_win32_leakcheck_stack_compare( + git_win32_leakcheck_stack_raw_data *d1, + git_win32_leakcheck_stack_raw_data *d2) +{ + return memcmp(d1, d2, sizeof(*d1)); +} + +int git_win32_leakcheck_stack_format( + char *pbuf, size_t buf_len, + const git_win32_leakcheck_stack_raw_data *pdata, + const char *prefix, const char *suffix) +{ +#define MY_MAX_FILENAME 255 + + /* SYMBOL_INFO has char FileName[1] at the end. The docs say to + * to malloc it with extra space for your desired max filename. + */ + struct { + SYMBOL_INFO symbol; + char extra[MY_MAX_FILENAME + 1]; + } s; + + IMAGEHLP_LINE64 line; + size_t buf_used = 0; + unsigned int k; + char detail[MY_MAX_FILENAME * 2]; /* filename plus space for function name and formatting */ + size_t detail_len; + + if (!g_win32_stack_initialized) { + git_error_set(GIT_ERROR_INVALID, "git_win32_stack not initialized."); + return GIT_ERROR; + } + + if (!prefix) + prefix = "\t"; + if (!suffix) + suffix = "\n"; + + memset(pbuf, 0, buf_len); + + memset(&s, 0, sizeof(s)); + s.symbol.MaxNameLen = MY_MAX_FILENAME; + s.symbol.SizeOfStruct = sizeof(SYMBOL_INFO); + + memset(&line, 0, sizeof(line)); + line.SizeOfStruct = sizeof(IMAGEHLP_LINE64); + + for (k=0; k < pdata->nr_frames; k++) { + DWORD64 frame_k = (DWORD64)pdata->frames[k]; + DWORD dwUnused; + + if (SymFromAddr(g_win32_stack_process, frame_k, 0, &s.symbol) && + SymGetLineFromAddr64(g_win32_stack_process, frame_k, &dwUnused, &line)) { + const char *pslash; + const char *pfile; + + pslash = strrchr(line.FileName, '\\'); + pfile = ((pslash) ? (pslash+1) : line.FileName); + p_snprintf(detail, sizeof(detail), "%s%s:%d> %s%s", + prefix, pfile, line.LineNumber, s.symbol.Name, suffix); + } else { + /* This happens when we cross into another module. + * For example, in CLAR tests, this is typically + * the CRT startup code. Just print an unknown + * frame and continue. + */ + p_snprintf(detail, sizeof(detail), "%s??%s", prefix, suffix); + } + detail_len = strlen(detail); + + if (buf_len < (buf_used + detail_len + 1)) { + /* we don't have room for this frame in the buffer, so just stop. */ + break; + } + + memcpy(&pbuf[buf_used], detail, detail_len); + buf_used += detail_len; + } + + /* "aux_id" 0 is reserved to mean no aux data. This is needed to handle + * allocs that occur before the aux callbacks were registered. + */ + if (pdata->aux_id > 0) { + p_snprintf(detail, sizeof(detail), "%saux_id: %d%s", + prefix, pdata->aux_id, suffix); + detail_len = strlen(detail); + if ((buf_used + detail_len + 1) < buf_len) { + memcpy(&pbuf[buf_used], detail, detail_len); + buf_used += detail_len; + } + + /* If an "aux" data provider is still registered, ask it to append its detailed + * data to the end of ours using the "aux_id" it gave us when this de-duped + * item was created. + */ + if (g_aux_cb_lookup) + (g_aux_cb_lookup)(pdata->aux_id, &pbuf[buf_used], (buf_len - buf_used - 1)); + } + + return GIT_OK; +} + +int git_win32_leakcheck_stack( + char * pbuf, size_t buf_len, + int skip, + const char *prefix, const char *suffix) +{ + git_win32_leakcheck_stack_raw_data data; + int error; + + if ((error = git_win32_leakcheck_stack_capture(&data, skip)) < 0) + return error; + if ((error = git_win32_leakcheck_stack_format(pbuf, buf_len, &data, prefix, suffix)) < 0) + return error; + return 0; +} + +/* Stack tracing */ + +#define STACKTRACE_UID_LEN (15) + +/** + * The stacktrace of an allocation can be distilled + * to a unique id based upon the stackframe pointers + * and ignoring any size arguments. We will use these + * UIDs as the (char const*) __FILE__ argument we + * give to the CRT malloc routines. + */ +typedef struct { + char uid[STACKTRACE_UID_LEN + 1]; +} git_win32_leakcheck_stacktrace_uid; + +/** + * All mallocs with the same stacktrace will be de-duped + * and aggregated into this row. + */ +typedef struct { + git_win32_leakcheck_stacktrace_uid uid; /* must be first */ + git_win32_leakcheck_stack_raw_data raw_data; + unsigned int count_allocs; /* times this alloc signature seen since init */ + unsigned int count_allocs_at_last_checkpoint; /* times since last mark */ + unsigned int transient_count_leaks; /* sum of leaks */ +} git_win32_leakcheck_stacktrace_row; + +static CRITICAL_SECTION g_crtdbg_stacktrace_cs; + +/** + * CRTDBG memory leak tracking takes a "char const * const file_name" + * and stores the pointer in the heap data (instead of allocing a copy + * for itself). Normally, this is not a problem, since we usually pass + * in __FILE__. But I'm going to lie to it and pass in the address of + * the UID in place of the file_name. Also, I do not want to alloc the + * stacktrace data (because we are called from inside our alloc routines). + * Therefore, I'm creating a very large static pool array to store row + * data. This also eliminates the temptation to realloc it (and move the + * UID pointers). + * + * And to efficiently look for duplicates we need an index on the rows + * so we can bsearch it. Again, without mallocing. + * + * If we observe more than MY_ROW_LIMIT unique malloc signatures, we + * fall through and use the traditional __FILE__ processing and don't + * try to de-dup them. If your testing hits this limit, just increase + * it and try again. + */ + +#define MY_ROW_LIMIT (2 * 1024 * 1024) +static git_win32_leakcheck_stacktrace_row g_cs_rows[MY_ROW_LIMIT]; +static git_win32_leakcheck_stacktrace_row *g_cs_index[MY_ROW_LIMIT]; + +static unsigned int g_cs_end = MY_ROW_LIMIT; +static unsigned int g_cs_ins = 0; /* insertion point == unique allocs seen */ +static unsigned int g_count_total_allocs = 0; /* number of allocs seen */ +static unsigned int g_transient_count_total_leaks = 0; /* number of total leaks */ +static unsigned int g_transient_count_dedup_leaks = 0; /* number of unique leaks */ +static bool g_limit_reached = false; /* had allocs after we filled row table */ + +static unsigned int g_checkpoint_id = 0; /* to better label leak checkpoints */ +static bool g_transient_leaks_since_mark = false; /* payload for hook */ + +/** + * Compare function for bsearch on g_cs_index table. + */ +static int row_cmp(const void *v1, const void *v2) +{ + git_win32_leakcheck_stack_raw_data *d1 = (git_win32_leakcheck_stack_raw_data*)v1; + git_win32_leakcheck_stacktrace_row *r2 = (git_win32_leakcheck_stacktrace_row *)v2; + + return (git_win32_leakcheck_stack_compare(d1, &r2->raw_data)); +} + +/** + * Unique insert the new data into the row and index tables. + * We have to sort by the stackframe data itself, not the uid. + */ +static git_win32_leakcheck_stacktrace_row * insert_unique( + const git_win32_leakcheck_stack_raw_data *pdata) +{ + size_t pos; + if (git__bsearch(g_cs_index, g_cs_ins, pdata, row_cmp, &pos) < 0) { + /* Append new unique item to row table. */ + memcpy(&g_cs_rows[g_cs_ins].raw_data, pdata, sizeof(*pdata)); + sprintf(g_cs_rows[g_cs_ins].uid.uid, "##%08lx", g_cs_ins); + + /* Insert pointer to it into the proper place in the index table. */ + if (pos < g_cs_ins) + memmove(&g_cs_index[pos+1], &g_cs_index[pos], (g_cs_ins - pos)*sizeof(g_cs_index[0])); + g_cs_index[pos] = &g_cs_rows[g_cs_ins]; + + g_cs_ins++; + } + + g_cs_index[pos]->count_allocs++; + + return g_cs_index[pos]; +} + +/** + * Hook function to receive leak data from the CRT. (This includes + * both ":()" data, but also each of the + * various headers and fields. + * + * Scan this for the special "##" UID forms that we substituted + * for the "". Map back to the row data and + * increment its leak count. + * + * See https://msdn.microsoft.com/en-us/library/74kabxyx.aspx + * + * We suppress the actual crtdbg output. + */ +static int __cdecl report_hook(int nRptType, char *szMsg, int *retVal) +{ + static int hook_result = TRUE; /* FALSE to get stock dump; TRUE to suppress. */ + unsigned int pos; + + *retVal = 0; /* do not invoke debugger */ + + if ((szMsg[0] != '#') || (szMsg[1] != '#')) + return hook_result; + + if (sscanf(&szMsg[2], "%08lx", &pos) < 1) + return hook_result; + if (pos >= g_cs_ins) + return hook_result; + + if (g_transient_leaks_since_mark) { + if (g_cs_rows[pos].count_allocs == g_cs_rows[pos].count_allocs_at_last_checkpoint) + return hook_result; + } + + g_cs_rows[pos].transient_count_leaks++; + + if (g_cs_rows[pos].transient_count_leaks == 1) + g_transient_count_dedup_leaks++; + + g_transient_count_total_leaks++; + + return hook_result; +} + +/** + * Write leak data to all of the various places we need. + * We force the caller to sprintf() the message first + * because we want to avoid fprintf() because it allocs. + */ +static void my_output(const char *buf) +{ + fwrite(buf, strlen(buf), 1, stderr); + OutputDebugString(buf); +} + +/** + * For each row with leaks, dump a stacktrace for it. + */ +static void dump_summary(const char *label) +{ + unsigned int k; + char buf[10 * 1024]; + + if (g_transient_count_total_leaks == 0) + return; + + fflush(stdout); + fflush(stderr); + my_output("\n"); + + if (g_limit_reached) { + sprintf(buf, + "LEAK SUMMARY: de-dup row table[%d] filled. Increase MY_ROW_LIMIT.\n", + MY_ROW_LIMIT); + my_output(buf); + } + + if (!label) + label = ""; + + if (g_transient_leaks_since_mark) { + sprintf(buf, "LEAK CHECKPOINT %d: leaks %d unique %d: %s\n", + g_checkpoint_id, g_transient_count_total_leaks, g_transient_count_dedup_leaks, label); + my_output(buf); + } else { + sprintf(buf, "LEAK SUMMARY: TOTAL leaks %d de-duped %d: %s\n", + g_transient_count_total_leaks, g_transient_count_dedup_leaks, label); + my_output(buf); + } + my_output("\n"); + + for (k = 0; k < g_cs_ins; k++) { + if (g_cs_rows[k].transient_count_leaks > 0) { + sprintf(buf, "LEAK: %s leaked %d of %d times:\n", + g_cs_rows[k].uid.uid, + g_cs_rows[k].transient_count_leaks, + g_cs_rows[k].count_allocs); + my_output(buf); + + if (git_win32_leakcheck_stack_format( + buf, sizeof(buf), &g_cs_rows[k].raw_data, + NULL, NULL) >= 0) { + my_output(buf); + } + + my_output("\n"); + } + } + + fflush(stderr); +} + +/** + * Initialize our memory leak tracking and de-dup data structures. + * This should ONLY be called by git_libgit2_init(). + */ +void git_win32_leakcheck_stacktrace_init(void) +{ + InitializeCriticalSection(&g_crtdbg_stacktrace_cs); + + EnterCriticalSection(&g_crtdbg_stacktrace_cs); + + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); + + _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE); + _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE); + _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE); + + _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); + _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR); + _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR); + + LeaveCriticalSection(&g_crtdbg_stacktrace_cs); +} + +int git_win32_leakcheck_stacktrace_dump( + git_win32_leakcheck_stacktrace_options opt, + const char *label) +{ + _CRT_REPORT_HOOK old; + unsigned int k; + int r = 0; + +#define IS_BIT_SET(o,b) (((o) & (b)) != 0) + + bool b_set_mark = IS_BIT_SET(opt, GIT_WIN32_LEAKCHECK_STACKTRACE_SET_MARK); + bool b_leaks_since_mark = IS_BIT_SET(opt, GIT_WIN32_LEAKCHECK_STACKTRACE_LEAKS_SINCE_MARK); + bool b_leaks_total = IS_BIT_SET(opt, GIT_WIN32_LEAKCHECK_STACKTRACE_LEAKS_TOTAL); + bool b_quiet = IS_BIT_SET(opt, GIT_WIN32_LEAKCHECK_STACKTRACE_QUIET); + + if (b_leaks_since_mark && b_leaks_total) { + git_error_set(GIT_ERROR_INVALID, "cannot combine LEAKS_SINCE_MARK and LEAKS_TOTAL."); + return GIT_ERROR; + } + if (!b_set_mark && !b_leaks_since_mark && !b_leaks_total) { + git_error_set(GIT_ERROR_INVALID, "nothing to do."); + return GIT_ERROR; + } + + EnterCriticalSection(&g_crtdbg_stacktrace_cs); + + if (b_leaks_since_mark || b_leaks_total) { + /* All variables with "transient" in the name are per-dump counters + * and reset before each dump. This lets us handle checkpoints. + */ + g_transient_count_total_leaks = 0; + g_transient_count_dedup_leaks = 0; + for (k = 0; k < g_cs_ins; k++) { + g_cs_rows[k].transient_count_leaks = 0; + } + } + + g_transient_leaks_since_mark = b_leaks_since_mark; + + old = _CrtSetReportHook(report_hook); + _CrtDumpMemoryLeaks(); + _CrtSetReportHook(old); + + if (b_leaks_since_mark || b_leaks_total) { + r = g_transient_count_dedup_leaks; + + if (!b_quiet) + dump_summary(label); + } + + if (b_set_mark) { + for (k = 0; k < g_cs_ins; k++) { + g_cs_rows[k].count_allocs_at_last_checkpoint = g_cs_rows[k].count_allocs; + } + + g_checkpoint_id++; + } + + LeaveCriticalSection(&g_crtdbg_stacktrace_cs); + + return r; +} + +/** + * Shutdown our memory leak tracking and dump summary data. + * This should ONLY be called by git_libgit2_shutdown(). + * + * We explicitly call _CrtDumpMemoryLeaks() during here so + * that we can compute summary data for the leaks. We print + * the stacktrace of each unique leak. + * + * This cleanup does not happen if the app calls exit() + * without calling the libgit2 shutdown code. + * + * This info we print here is independent of any automatic + * reporting during exit() caused by _CRTDBG_LEAK_CHECK_DF. + * Set it in your app if you also want traditional reporting. + */ +void git_win32_leakcheck_stacktrace_cleanup(void) +{ + /* At shutdown/cleanup, dump cumulative leak info + * with everything since startup. This might generate + * extra noise if the caller has been doing checkpoint + * dumps, but it might also eliminate some false + * positives for resources previously reported during + * checkpoints. + */ + git_win32_leakcheck_stacktrace_dump( + GIT_WIN32_LEAKCHECK_STACKTRACE_LEAKS_TOTAL, + "CLEANUP"); + + DeleteCriticalSection(&g_crtdbg_stacktrace_cs); +} + +const char *git_win32_leakcheck_stacktrace(int skip, const char *file) +{ + git_win32_leakcheck_stack_raw_data new_data; + git_win32_leakcheck_stacktrace_row *row; + const char * result = file; + + if (git_win32_leakcheck_stack_capture(&new_data, skip+1) < 0) + return result; + + EnterCriticalSection(&g_crtdbg_stacktrace_cs); + + if (g_cs_ins < g_cs_end) { + row = insert_unique(&new_data); + result = row->uid.uid; + } else { + g_limit_reached = true; + } + + g_count_total_allocs++; + + LeaveCriticalSection(&g_crtdbg_stacktrace_cs); + + return result; +} + +static void git_win32_leakcheck_global_shutdown(void) +{ + git_win32_leakcheck_stacktrace_cleanup(); + git_win32_leakcheck_stack_cleanup(); +} + +bool git_win32_leakcheck_has_leaks(void) +{ + return (g_transient_count_total_leaks > 0); +} + +int git_win32_leakcheck_global_init(void) +{ + git_win32_leakcheck_stacktrace_init(); + git_win32_leakcheck_stack_init(); + + return git_runtime_shutdown_register(git_win32_leakcheck_global_shutdown); +} + +#else + +int git_win32_leakcheck_global_init(void) +{ + return 0; +} + +#endif diff --git a/src/util/win32/w32_leakcheck.h b/src/util/win32/w32_leakcheck.h new file mode 100644 index 0000000..82d8638 --- /dev/null +++ b/src/util/win32/w32_leakcheck.h @@ -0,0 +1,222 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_win32_leakcheck_h__ +#define INCLUDE_win32_leakcheck_h__ + +#include "git2_util.h" + +/* Initialize the win32 leak checking system. */ +int git_win32_leakcheck_global_init(void); + +#if defined(GIT_WIN32_LEAKCHECK) + +#include +#include + +#include "git2/errors.h" +#include "strnlen.h" + +bool git_win32_leakcheck_has_leaks(void); + +/* Stack frames (for stack tracing, below) */ + +/** + * This type defines a callback to be used to augment a C stacktrace + * with "aux" data. This can be used, for example, to allow LibGit2Sharp + * (or other interpreted consumer libraries) to give us C# stacktrace + * data for the PInvoke. + * + * This callback will be called during crtdbg-instrumented allocs. + * + * @param aux_id [out] A returned "aux_id" representing a unique + * (de-duped at the C# layer) stacktrace. "aux_id" 0 is reserved + * to mean no aux stacktrace data. + */ +typedef void (*git_win32_leakcheck_stack_aux_cb_alloc)(unsigned int *aux_id); + +/** + * This type defines a callback to be used to augment the output of + * a stacktrace. This will be used to request the C# layer format + * the C# stacktrace associated with "aux_id" into the provided + * buffer. + * + * This callback will be called during leak reporting. + * + * @param aux_id The "aux_id" key associated with a stacktrace. + * @param aux_msg A buffer where a formatted message should be written. + * @param aux_msg_len The size of the buffer. + */ +typedef void (*git_win32_leakcheck_stack_aux_cb_lookup)(unsigned int aux_id, char *aux_msg, size_t aux_msg_len); + +/** + * Register an "aux" data provider to augment our C stacktrace data. + * + * This can be used, for example, to allow LibGit2Sharp (or other + * interpreted consumer libraries) to give us the C# stacktrace of + * the PInvoke. + * + * If you choose to use this feature, it should be registered during + * initialization and not changed for the duration of the process. + */ +int git_win32_leakcheck_stack_set_aux_cb( + git_win32_leakcheck_stack_aux_cb_alloc cb_alloc, + git_win32_leakcheck_stack_aux_cb_lookup cb_lookup); + +/** + * Maximum number of stackframes to record for a + * single stacktrace. + */ +#define GIT_WIN32_LEAKCHECK_STACK_MAX_FRAMES 30 + +/** + * Wrapper containing the raw unprocessed stackframe + * data for a single stacktrace and any "aux_id". + * + * I put the aux_id first so leaks will be sorted by it. + * So, for example, if a specific callstack in C# leaks + * a repo handle, all of the pointers within the associated + * repo pointer will be grouped together. + */ +typedef struct { + unsigned int aux_id; + unsigned int nr_frames; + void *frames[GIT_WIN32_LEAKCHECK_STACK_MAX_FRAMES]; +} git_win32_leakcheck_stack_raw_data; + +/** + * Capture raw stack trace data for the current process/thread. + * + * @param skip Number of initial frames to skip. Pass 0 to + * begin with the caller of this routine. Pass 1 to begin + * with its caller. And so on. + */ +int git_win32_leakcheck_stack_capture(git_win32_leakcheck_stack_raw_data *pdata, int skip); + +/** + * Compare 2 raw stacktraces with the usual -1,0,+1 result. + * This includes any "aux_id" values in the comparison, so that + * our de-dup is also "aux" context relative. + */ +int git_win32_leakcheck_stack_compare( + git_win32_leakcheck_stack_raw_data *d1, + git_win32_leakcheck_stack_raw_data *d2); + +/** + * Format raw stacktrace data into buffer WITHOUT using any mallocs. + * + * @param prefix String written before each frame; defaults to "\t". + * @param suffix String written after each frame; defaults to "\n". + */ +int git_win32_leakcheck_stack_format( + char *pbuf, size_t buf_len, + const git_win32_leakcheck_stack_raw_data *pdata, + const char *prefix, const char *suffix); + +/** + * Convenience routine to capture and format stacktrace into + * a buffer WITHOUT using any mallocs. This is primarily a + * wrapper for testing. + * + * @param skip Number of initial frames to skip. Pass 0 to + * begin with the caller of this routine. Pass 1 to begin + * with its caller. And so on. + * @param prefix String written before each frame; defaults to "\t". + * @param suffix String written after each frame; defaults to "\n". + */ +int git_win32_leakcheck_stack( + char * pbuf, size_t buf_len, + int skip, + const char *prefix, const char *suffix); + +/* Stack tracing */ + +/* MSVC CRTDBG memory leak reporting. + * + * We DO NOT use the "_CRTDBG_MAP_ALLOC" macro described in the MSVC + * documentation because all allocs/frees in libgit2 already go through + * the "git__" routines defined in this file. Simply using the normal + * reporting mechanism causes all leaks to be attributed to a routine + * here in util.h (ie, the actual call to calloc()) rather than the + * caller of git__calloc(). + * + * Therefore, we declare a set of "git__crtdbg__" routines to replace + * the corresponding "git__" routines and re-define the "git__" symbols + * as macros. This allows us to get and report the file:line info of + * the real caller. + * + * We DO NOT replace the "git__free" routine because it needs to remain + * a function pointer because it is used as a function argument when + * setting up various structure "destructors". + * + * We also DO NOT use the "_CRTDBG_MAP_ALLOC" macro because it causes + * "free" to be remapped to "_free_dbg" and this causes problems for + * structures which define a field named "free". + * + * Finally, CRTDBG must be explicitly enabled and configured at program + * startup. See tests/main.c for an example. + */ + +/** + * Checkpoint options. + */ +typedef enum git_win32_leakcheck_stacktrace_options { + /** + * Set checkpoint marker. + */ + GIT_WIN32_LEAKCHECK_STACKTRACE_SET_MARK = (1 << 0), + + /** + * Dump leaks since last checkpoint marker. + * May not be combined with _LEAKS_TOTAL. + * + * Note that this may generate false positives for global TLS + * error state and other global caches that aren't cleaned up + * until the thread/process terminates. So when using this + * around a region of interest, also check the final (at exit) + * dump before digging into leaks reported here. + */ + GIT_WIN32_LEAKCHECK_STACKTRACE_LEAKS_SINCE_MARK = (1 << 1), + + /** + * Dump leaks since init. May not be combined + * with _LEAKS_SINCE_MARK. + */ + GIT_WIN32_LEAKCHECK_STACKTRACE_LEAKS_TOTAL = (1 << 2), + + /** + * Suppress printing during dumps. + * Just return leak count. + */ + GIT_WIN32_LEAKCHECK_STACKTRACE_QUIET = (1 << 3), + +} git_win32_leakcheck_stacktrace_options; + +/** + * Checkpoint memory state and/or dump unique stack traces of + * current memory leaks. + * + * @return number of unique leaks (relative to requested starting + * point) or error. + */ +int git_win32_leakcheck_stacktrace_dump( + git_win32_leakcheck_stacktrace_options opt, + const char *label); + +/** + * Construct stacktrace and append it to the global buffer. + * Return pointer to start of this string. On any error or + * lack of buffer space, just return the given file buffer + * so it will behave as usual. + * + * This should ONLY be called by our internal memory allocations + * routines. + */ +const char *git_win32_leakcheck_stacktrace(int skip, const char *file); + +#endif +#endif diff --git a/src/util/win32/w32_util.c b/src/util/win32/w32_util.c new file mode 100644 index 0000000..f5b006a --- /dev/null +++ b/src/util/win32/w32_util.c @@ -0,0 +1,126 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "w32_util.h" + +/** + * Creates a FindFirstFile(Ex) filter string from a UTF-8 path. + * The filter string enumerates all items in the directory. + * + * @param dest The buffer to receive the filter string. + * @param src The UTF-8 path of the directory to enumerate. + * @return True if the filter string was created successfully; false otherwise + */ +bool git_win32__findfirstfile_filter(git_win32_path dest, const char *src) +{ + static const wchar_t suffix[] = L"\\*"; + int len = git_win32_path_from_utf8(dest, src); + + /* Ensure the path was converted */ + if (len < 0) + return false; + + /* Ensure that the path does not end with a trailing slash, + * because we're about to add one. Don't rely our trim_end + * helper, because we want to remove the backslash even for + * drive letter paths, in this case. */ + if (len > 0 && + (dest[len - 1] == L'/' || dest[len - 1] == L'\\')) { + dest[len - 1] = L'\0'; + len--; + } + + /* Ensure we have enough room to add the suffix */ + if ((size_t)len >= GIT_WIN_PATH_UTF16 - CONST_STRLEN(suffix)) + return false; + + wcscat(dest, suffix); + return true; +} + +/** + * Ensures the given path (file or folder) has the +H (hidden) attribute set. + * + * @param path The path which should receive the +H bit. + * @return 0 on success; -1 on failure + */ +int git_win32__set_hidden(const char *path, bool hidden) +{ + git_win32_path buf; + DWORD attrs, newattrs; + + if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + attrs = GetFileAttributesW(buf); + + /* Ensure the path exists */ + if (attrs == INVALID_FILE_ATTRIBUTES) + return -1; + + if (hidden) + newattrs = attrs | FILE_ATTRIBUTE_HIDDEN; + else + newattrs = attrs & ~FILE_ATTRIBUTE_HIDDEN; + + if (attrs != newattrs && !SetFileAttributesW(buf, newattrs)) { + git_error_set(GIT_ERROR_OS, "failed to %s hidden bit for '%s'", + hidden ? "set" : "unset", path); + return -1; + } + + return 0; +} + +int git_win32__hidden(bool *out, const char *path) +{ + git_win32_path buf; + DWORD attrs; + + if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + attrs = GetFileAttributesW(buf); + + /* Ensure the path exists */ + if (attrs == INVALID_FILE_ATTRIBUTES) + return -1; + + *out = (attrs & FILE_ATTRIBUTE_HIDDEN) ? true : false; + return 0; +} + +int git_win32__file_attribute_to_stat( + struct stat *st, + const WIN32_FILE_ATTRIBUTE_DATA *attrdata, + const wchar_t *path) +{ + git_win32__stat_init(st, + attrdata->dwFileAttributes, + attrdata->nFileSizeHigh, + attrdata->nFileSizeLow, + attrdata->ftCreationTime, + attrdata->ftLastAccessTime, + attrdata->ftLastWriteTime); + + if (attrdata->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT && path) { + git_win32_path target; + + if (git_win32_path_readlink_w(target, path) >= 0) { + st->st_mode = (st->st_mode & ~S_IFMT) | S_IFLNK; + + /* st_size gets the UTF-8 length of the target name, in bytes, + * not counting the NULL terminator */ + if ((st->st_size = git_utf8_from_16(NULL, 0, target)) < 0) { + git_error_set(GIT_ERROR_OS, "could not convert reparse point name for '%ls'", path); + return -1; + } + } + } + + return 0; +} diff --git a/src/util/win32/w32_util.h b/src/util/win32/w32_util.h new file mode 100644 index 0000000..5196637 --- /dev/null +++ b/src/util/win32/w32_util.h @@ -0,0 +1,144 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_win32_w32_util_h__ +#define INCLUDE_win32_w32_util_h__ + +#include "git2_util.h" + +#include "utf-conv.h" +#include "posix.h" +#include "path_w32.h" + +/* + +#include "common.h" +#include "path.h" +#include "path_w32.h" +#include "utf-conv.h" +#include "posix.h" +#include "reparse.h" +#include "dir.h" +*/ + + +GIT_INLINE(bool) git_win32__isalpha(wchar_t c) +{ + return ((c >= L'A' && c <= L'Z') || (c >= L'a' && c <= L'z')); +} + +/** + * Creates a FindFirstFile(Ex) filter string from a UTF-8 path. + * The filter string enumerates all items in the directory. + * + * @param dest The buffer to receive the filter string. + * @param src The UTF-8 path of the directory to enumerate. + * @return True if the filter string was created successfully; false otherwise + */ +bool git_win32__findfirstfile_filter(git_win32_path dest, const char *src); + +/** + * Ensures the given path (file or folder) has the +H (hidden) attribute set + * or unset. + * + * @param path The path that should receive the +H bit. + * @param hidden true to set +H, false to unset it + * @return 0 on success; -1 on failure + */ +extern int git_win32__set_hidden(const char *path, bool hidden); + +/** + * Determines if the given file or folder has the hidden attribute set. + * @param hidden pointer to store hidden value + * @param path The path that should be queried for hiddenness. + * @return 0 on success or an error code. + */ +extern int git_win32__hidden(bool *hidden, const char *path); + +extern int git_win32__file_attribute_to_stat( + struct stat *st, + const WIN32_FILE_ATTRIBUTE_DATA *attrdata, + const wchar_t *path); + +/** + * Converts a FILETIME structure to a struct timespec. + * + * @param FILETIME A pointer to a FILETIME + * @param ts A pointer to the timespec structure to fill in + */ +GIT_INLINE(void) git_win32__filetime_to_timespec( + const FILETIME *ft, + struct timespec *ts) +{ + int64_t winTime = ((int64_t)ft->dwHighDateTime << 32) + ft->dwLowDateTime; + winTime -= INT64_C(116444736000000000); /* Windows to Unix Epoch conversion */ + ts->tv_sec = (time_t)(winTime / 10000000); +#ifdef GIT_USE_NSEC + ts->tv_nsec = (winTime % 10000000) * 100; +#else + ts->tv_nsec = 0; +#endif +} + +GIT_INLINE(void) git_win32__timeval_to_filetime( + FILETIME *ft, const struct p_timeval tv) +{ + int64_t ticks = (tv.tv_sec * INT64_C(10000000)) + + (tv.tv_usec * INT64_C(10)) + INT64_C(116444736000000000); + + ft->dwHighDateTime = ((ticks >> 32) & INT64_C(0xffffffff)); + ft->dwLowDateTime = (ticks & INT64_C(0xffffffff)); +} + +GIT_INLINE(void) git_win32__stat_init( + struct stat *st, + DWORD dwFileAttributes, + DWORD nFileSizeHigh, + DWORD nFileSizeLow, + FILETIME ftCreationTime, + FILETIME ftLastAccessTime, + FILETIME ftLastWriteTime) +{ + mode_t mode = S_IREAD; + + memset(st, 0, sizeof(struct stat)); + + if (dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + mode |= S_IFDIR; + else + mode |= S_IFREG; + + if ((dwFileAttributes & FILE_ATTRIBUTE_READONLY) == 0) + mode |= S_IWRITE; + + st->st_ino = 0; + st->st_gid = 0; + st->st_uid = 0; + st->st_nlink = 1; + st->st_mode = mode; + st->st_size = ((int64_t)nFileSizeHigh << 32) + nFileSizeLow; + st->st_dev = _getdrive() - 1; + st->st_rdev = st->st_dev; + git_win32__filetime_to_timespec(&ftLastAccessTime, &(st->st_atim)); + git_win32__filetime_to_timespec(&ftLastWriteTime, &(st->st_mtim)); + git_win32__filetime_to_timespec(&ftCreationTime, &(st->st_ctim)); +} + +GIT_INLINE(void) git_win32__file_information_to_stat( + struct stat *st, + const BY_HANDLE_FILE_INFORMATION *fileinfo) +{ + git_win32__stat_init(st, + fileinfo->dwFileAttributes, + fileinfo->nFileSizeHigh, + fileinfo->nFileSizeLow, + fileinfo->ftCreationTime, + fileinfo->ftLastAccessTime, + fileinfo->ftLastWriteTime); +} + +#endif diff --git a/src/util/win32/win32-compat.h b/src/util/win32/win32-compat.h new file mode 100644 index 0000000..dee40a4 --- /dev/null +++ b/src/util/win32/win32-compat.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_win32_win32_compat_h__ +#define INCLUDE_win32_win32_compat_h__ + +#include +#include +#include +#include +#include + +typedef long suseconds_t; + +struct p_timeval { + time_t tv_sec; + suseconds_t tv_usec; +}; + +struct p_timespec { + time_t tv_sec; + long tv_nsec; +}; + +#define timespec p_timespec + +struct p_stat { + _dev_t st_dev; + _ino_t st_ino; + mode_t st_mode; + short st_nlink; + short st_uid; + short st_gid; + _dev_t st_rdev; + __int64 st_size; + struct timespec st_atim; + struct timespec st_mtim; + struct timespec st_ctim; +#define st_atime st_atim.tv_sec +#define st_mtime st_mtim.tv_sec +#define st_ctime st_ctim.tv_sec +#define st_atime_nsec st_atim.tv_nsec +#define st_mtime_nsec st_mtim.tv_nsec +#define st_ctime_nsec st_ctim.tv_nsec +}; + +#define stat p_stat + +#endif diff --git a/src/util/zstream.c b/src/util/zstream.c new file mode 100644 index 0000000..cb8b125 --- /dev/null +++ b/src/util/zstream.c @@ -0,0 +1,210 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "zstream.h" + +#include + +#include "str.h" + +#define ZSTREAM_BUFFER_SIZE (1024 * 1024) +#define ZSTREAM_BUFFER_MIN_EXTRA 8 + +GIT_INLINE(int) zstream_seterr(git_zstream *zs) +{ + switch (zs->zerr) { + case Z_OK: + case Z_STREAM_END: + case Z_BUF_ERROR: /* not fatal; we retry with a larger buffer */ + return 0; + case Z_MEM_ERROR: + git_error_set_oom(); + break; + default: + if (zs->z.msg) + git_error_set_str(GIT_ERROR_ZLIB, zs->z.msg); + else + git_error_set(GIT_ERROR_ZLIB, "unknown compression error"); + } + + return -1; +} + +int git_zstream_init(git_zstream *zstream, git_zstream_t type) +{ + zstream->type = type; + + if (zstream->type == GIT_ZSTREAM_INFLATE) + zstream->zerr = inflateInit(&zstream->z); + else + zstream->zerr = deflateInit(&zstream->z, Z_DEFAULT_COMPRESSION); + return zstream_seterr(zstream); +} + +void git_zstream_free(git_zstream *zstream) +{ + if (zstream->type == GIT_ZSTREAM_INFLATE) + inflateEnd(&zstream->z); + else + deflateEnd(&zstream->z); +} + +void git_zstream_reset(git_zstream *zstream) +{ + if (zstream->type == GIT_ZSTREAM_INFLATE) + inflateReset(&zstream->z); + else + deflateReset(&zstream->z); + zstream->in = NULL; + zstream->in_len = 0; + zstream->zerr = Z_STREAM_END; +} + +int git_zstream_set_input(git_zstream *zstream, const void *in, size_t in_len) +{ + zstream->in = in; + zstream->in_len = in_len; + zstream->zerr = Z_OK; + return 0; +} + +bool git_zstream_done(git_zstream *zstream) +{ + return (!zstream->in_len && zstream->zerr == Z_STREAM_END); +} + +bool git_zstream_eos(git_zstream *zstream) +{ + return zstream->zerr == Z_STREAM_END; +} + +size_t git_zstream_suggest_output_len(git_zstream *zstream) +{ + if (zstream->in_len > ZSTREAM_BUFFER_SIZE) + return ZSTREAM_BUFFER_SIZE; + else if (zstream->in_len > ZSTREAM_BUFFER_MIN_EXTRA) + return zstream->in_len; + else + return ZSTREAM_BUFFER_MIN_EXTRA; +} + +int git_zstream_get_output_chunk( + void *out, size_t *out_len, git_zstream *zstream) +{ + size_t in_queued, in_used, out_queued; + + /* set up input data */ + zstream->z.next_in = (Bytef *)zstream->in; + + /* feed as much data to zlib as it can consume, at most UINT_MAX */ + if (zstream->in_len > UINT_MAX) { + zstream->z.avail_in = UINT_MAX; + zstream->flush = Z_NO_FLUSH; + } else { + zstream->z.avail_in = (uInt)zstream->in_len; + zstream->flush = Z_FINISH; + } + in_queued = (size_t)zstream->z.avail_in; + + /* set up output data */ + zstream->z.next_out = out; + zstream->z.avail_out = (uInt)*out_len; + + if ((size_t)zstream->z.avail_out != *out_len) + zstream->z.avail_out = UINT_MAX; + out_queued = (size_t)zstream->z.avail_out; + + /* compress next chunk */ + if (zstream->type == GIT_ZSTREAM_INFLATE) + zstream->zerr = inflate(&zstream->z, zstream->flush); + else + zstream->zerr = deflate(&zstream->z, zstream->flush); + + if (zstream_seterr(zstream)) + return -1; + + in_used = (in_queued - zstream->z.avail_in); + zstream->in_len -= in_used; + zstream->in += in_used; + + *out_len = (out_queued - zstream->z.avail_out); + + return 0; +} + +int git_zstream_get_output(void *out, size_t *out_len, git_zstream *zstream) +{ + size_t out_remain = *out_len; + + if (zstream->in_len && zstream->zerr == Z_STREAM_END) { + git_error_set(GIT_ERROR_ZLIB, "zlib input had trailing garbage"); + return -1; + } + + while (out_remain > 0 && zstream->zerr != Z_STREAM_END) { + size_t out_written = out_remain; + + if (git_zstream_get_output_chunk(out, &out_written, zstream) < 0) + return -1; + + out_remain -= out_written; + out = ((char *)out) + out_written; + } + + /* either we finished the input or we did not flush the data */ + GIT_ASSERT(zstream->in_len > 0 || zstream->flush == Z_FINISH); + + /* set out_size to number of bytes actually written to output */ + *out_len = *out_len - out_remain; + + return 0; +} + +static int zstream_buf(git_str *out, const void *in, size_t in_len, git_zstream_t type) +{ + git_zstream zs = GIT_ZSTREAM_INIT; + int error = 0; + + if ((error = git_zstream_init(&zs, type)) < 0) + return error; + + if ((error = git_zstream_set_input(&zs, in, in_len)) < 0) + goto done; + + while (!git_zstream_done(&zs)) { + size_t step = git_zstream_suggest_output_len(&zs), written; + + if ((error = git_str_grow_by(out, step)) < 0) + goto done; + + written = out->asize - out->size; + + if ((error = git_zstream_get_output( + out->ptr + out->size, &written, &zs)) < 0) + goto done; + + out->size += written; + } + + /* NULL terminate for consistency if possible */ + if (out->size < out->asize) + out->ptr[out->size] = '\0'; + +done: + git_zstream_free(&zs); + return error; +} + +int git_zstream_deflatebuf(git_str *out, const void *in, size_t in_len) +{ + return zstream_buf(out, in, in_len, GIT_ZSTREAM_DEFLATE); +} + +int git_zstream_inflatebuf(git_str *out, const void *in, size_t in_len) +{ + return zstream_buf(out, in, in_len, GIT_ZSTREAM_INFLATE); +} diff --git a/src/util/zstream.h b/src/util/zstream.h new file mode 100644 index 0000000..d78b112 --- /dev/null +++ b/src/util/zstream.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_zstream_h__ +#define INCLUDE_zstream_h__ + +#include "git2_util.h" + +#include + +#include "str.h" + +typedef enum { + GIT_ZSTREAM_INFLATE, + GIT_ZSTREAM_DEFLATE +} git_zstream_t; + +typedef struct { + z_stream z; + git_zstream_t type; + const char *in; + size_t in_len; + int flush; + int zerr; +} git_zstream; + +#define GIT_ZSTREAM_INIT {{0}} + +int git_zstream_init(git_zstream *zstream, git_zstream_t type); +void git_zstream_free(git_zstream *zstream); + +int git_zstream_set_input(git_zstream *zstream, const void *in, size_t in_len); + +size_t git_zstream_suggest_output_len(git_zstream *zstream); + +/* get as much output as is available in the input buffer */ +int git_zstream_get_output_chunk( + void *out, size_t *out_len, git_zstream *zstream); + +/* get all the output from the entire input buffer */ +int git_zstream_get_output(void *out, size_t *out_len, git_zstream *zstream); + +bool git_zstream_done(git_zstream *zstream); +bool git_zstream_eos(git_zstream *zstream); + +void git_zstream_reset(git_zstream *zstream); + +int git_zstream_deflatebuf(git_str *out, const void *in, size_t in_len); +int git_zstream_inflatebuf(git_str *out, const void *in, size_t in_len); + +#endif -- cgit v1.2.3