diff --git a/Documentation/Makefile b/Documentation/Makefile index cd5b4396db..3f599524ea 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -17,6 +17,7 @@ DOC_HTML=$(MAN_HTML) ARTICLES = howto-index ARTICLES += everyday ARTICLES += git-tools +ARTICLES += git-bisect-lk2009 # with their own formatting rules. SP_ARTICLES = howto/revert-branch-rebase howto/using-merge-subtree user-manual API_DOCS = $(patsubst %.txt,%,$(filter-out technical/api-index-skel.txt technical/api-index.txt, $(wildcard technical/api-*.txt))) diff --git a/Documentation/RelNotes-1.6.5.4.txt b/Documentation/RelNotes-1.6.5.4.txt new file mode 100644 index 0000000000..e42f8b2397 --- /dev/null +++ b/Documentation/RelNotes-1.6.5.4.txt @@ -0,0 +1,32 @@ +Git v1.6.5.4 Release Notes +========================== + +Fixes since v1.6.5.3 +-------------------- + + * "git help" (without argument) used to check if you are in a directory + under git control. There was no breakage in behaviour per-se, but this + was unnecessary. + + * "git prune-packed" gave progress output even when its standard error is + not connected to a terminal; this caused cron jobs that run it to + produce crufts. + + * "git pack-objects --all-progress" is an option to ask progress output + from write-object phase _if_ progress output were to be produced, and + shouldn't have forced the progress output. + + * "git apply -p --directory=" did not work well for a + non-default value of n. + + * "git merge foo HEAD" was misparsed as an old-style invocation of the + command and produced a confusing error message. As it does not specify + any other branch to merge, it shouldn't be mistaken as such. We will + remove the old style "git merge HEAD ..." syntax in + future versions, but not in this release, + + * "git merge -m ..." added the standard merge message + on its own after user-supplied message, which should have overrided the + standard one. + +Other minor documentation updates are included. diff --git a/Documentation/RelNotes-1.6.6.txt b/Documentation/RelNotes-1.6.6.txt index 371101d667..afcce8ba9a 100644 --- a/Documentation/RelNotes-1.6.6.txt +++ b/Documentation/RelNotes-1.6.6.txt @@ -1,39 +1,106 @@ Git v1.6.6 Release Notes ======================== -In this release, "git fsck" defaults to "git fsck --full" and checks -packfiles, and because of this it will take much longer to complete -than before. If you prefer a quicker check only on loose objects (the -old default), you can say "git fsck --no-full". This has been -supported by 1.5.4 and newer versions of git, so it is safe to write -it in your script even if you use slightly older git on some of your -machines. +Notes on behaviour change +------------------------- -In git 1.7.0, which is planned to be the release after 1.6.6, "git -push" into a branch that is currently checked out will be refused by -default. + * In this release, "git fsck" defaults to "git fsck --full" and + checks packfiles, and because of this it will take much longer to + complete than before. If you prefer a quicker check only on loose + objects (the old default), you can say "git fsck --no-full". This + has been supported by 1.5.4 and newer versions of git, so it is + safe to write it in your script even if you use slightly older git + on some of your machines. -You can choose what should happen upon such a push by setting the -configuration variable receive.denyCurrentBranch in the receiving -repository. +Preparing yourselves for compatibility issues in 1.7.0 +------------------------------------------------------ -Also, "git push $there :$killed" to delete the branch $killed in a remote -repository $there, when $killed branch is the current branch pointed at by -its HEAD, will be refused by default. +In git 1.7.0, which is planned to be the release after 1.6.6, there will +be a handful of behaviour changes that will break backward compatibility. -You can choose what should happen upon such a push by setting the -configuration variable receive.denyDeleteCurrent in the receiving -repository. +These changes were discussed long time ago and existing behaviours have +been identified as more problematic to the userbase than keeping them for +the sake of backward compatibility. -To ease the transition plan, the receiving repository of such a -push running this release will issue a big warning when the -configuration variable is missing. Please refer to: +When necessary, transition strategy for existing users has been designed +not to force them running around setting configuration variables and +updating their scripts in order to either keep the traditional behaviour +or use the new behaviour on the day their sysadmin decides to install +the new version of git. When we switched from "git-foo" to "git foo" in +1.6.0, even though the change had been advertised and the transition +guide had been provided for a very long time, the users procrastinated +during the entire transtion period, and ended up panicking on the day +their sysadmins updated their git installation. We tried very hard to +avoid repeating that unpleasantness. - http://git.or.cz/gitwiki/GitFaq#non-bare - http://thread.gmane.org/gmane.comp.version-control.git/107758/focus=108007 +For changes decided to be in 1.7.0, we have been much louder to strongly +discourage such procrastination. If you have been using recent versions +of git, you would have already seen warnings issued when you exercised +features whose behaviour will change, with the instruction on how to +keep the existing behaviour if you want to. You hopefully should be +well prepared already. + +Of course, we have also given "this and that will change in 1.7.0; +prepare yourselves" warnings in the release notes and announcement +messages. Let's see how well users will fare this time. + + * "git push" into a branch that is currently checked out (i.e. pointed by + HEAD in a repository that is not bare) will be refused by default. + + Similarly, "git push $there :$killed" to delete the branch $killed + in a remote repository $there, when $killed branch is the current + branch pointed at by its HEAD, will be refused by default. + + Setting the configuration variables receive.denyCurrentBranch and + receive.denyDeleteCurrent to 'ignore' in the receiving repository + can be used to override these safety features. Versions of git + since 1.6.2 have issued a loud warning when you tried to do them + without setting the configuration, so repositories of people who + still need to be able to perform such a push should already have + been future proofed. + + Please refer to: + + http://git.or.cz/gitwiki/GitFaq#non-bare + http://thread.gmane.org/gmane.comp.version-control.git/107758/focus=108007 + + for more details on the reason why this change is needed and the + transition process that already took place so far. + + * "git send-email" will not make deep threads by default when sending a + patch series with more than two messages. All messages will be sent + as a reply to the first message, i.e. cover letter. Git 1.6.6 (this + release) will issue a warning about the upcoming default change, when + it uses the traditional "deep threading" behaviour as the built-in + default. To squelch the warning but still use the "deep threading" + behaviour, give --chain-reply-to option or set sendemail.chainreplyto + to true. + + It has been possible to configure send-email to send "shallow thread" + by setting sendemail.chainreplyto configuration variable to false. + The only thing 1.7.0 release will do is to change the default when + you haven't configured that variable. + + * "git status" will not be "git commit --dry-run". This change does not + affect you if you run the command without pathspec. + + Nobody sane found the current behaviour of "git status Makefile" useful + nor meaningful, and it confused users. "git commit --dry-run" has been + provided as a way to get the current behaviour of this command since + 1.6.5. + + * "git diff" traditionally treated various "ignore whitespace" options + only as a way to filter the patch output. "git diff --exit-code -b" + exited with non-zero status even if all changes were about changing the + ammount of whitespace and nothing else. and "git diff -b" showed the + "diff --git" header line for such a change without patch text. + + In 1.7.0, the "ignore whitespaces" will affect the semantics of the + diff operation itself. A change that does not affect anything but + whitespaces will be reported with zero exit status when run with + --exit-code, and there will not be "diff --git" header for such a + change. -for more details on the reason why this change is needed and the -transition plan. Updates since v1.6.5 -------------------- @@ -71,11 +138,25 @@ Updates since v1.6.5 is only one remote tracking branch "frotz" is taken as a request to start the named branch at the corresponding remote tracking branch. + * "git commit -c/-C/--amend" can be told with a new "--reset-author" option + to ignore authorship information in the commit it is taking the message + from. + * "git describe" can be told to add "-dirty" suffix with "--dirty" option. * "git diff" learned --submodule option to show a list of one-line logs instead of differences between the commit object names. + * "git diff" learned to honor diff.color.func configuration to paint + function name hint printed on the hunk header "@@ -j,k +l,m @@" line + in the specified color. + + * "git fetch" learned --all and --multiple options, to run fetch from + many repositories, and --prune option to remove remote tracking + branches that went stale. These make "git remote update" and "git + remote prune" less necessary (there is no plan to remove "remote + update" nor "remote prune", though). + * "git fsck" by default checks the packfiles (i.e. "--full" is the default); you can turn it off with "git fsck --no-full". @@ -88,6 +169,9 @@ Updates since v1.6.5 * "git log --decorate" shows the location of HEAD as well. + * "git log" and "git rev-list" learned to take revs and pathspecs from + the standard input with the new "--stdin" option. + * "--pretty=format" option to "log" family of commands learned: . to wrap text with the "%w()" specifier. @@ -98,6 +182,10 @@ Updates since v1.6.5 * "git merge" (and "git pull") learned --ff-only option to make it fail if the merge does not result in a fast-forward. + * The ancient "git merge HEAD ..." syntax will be + removed in later versions of git. A warning is given and tells + users to use the "git merge -m ..." instead. + * "git mergetool" learned to use p4merge. * "git rebase -i" learned "reword" that acts like "edit" but immediately @@ -105,11 +193,21 @@ Updates since v1.6.5 the shell, which is done by "edit" to give an opportunity to tweak the contents. + * "git send-email" can be told with "--envelope-sender=auto" to use the + same address as "From:" address as the envelope sender address. + + * "git send-email" will issue a warning when it defaults to the + --chain-reply-to behaviour without being told by the user and + instructs to prepare for the change of the default in 1.7.0 release. + * In "git submodule add ", is now optional and inferred from the same way "git clone " does. * "git svn" learned to read SVN 1.5+ and SVK merge tickets. + * "gitweb" can optionally render its "blame" output incrementally (this + requires JavaScript on the client side). + * Author names shown in gitweb output are links to search commits by the author. @@ -122,8 +220,24 @@ Fixes since v1.6.5 All of the fixes in v1.6.5.X maintenance series are included in this release, unless otherwise noted. + * Enumeration of available merge strategies iterated over the list of + commands in a wrong way, sometimes producing an incorrect result. + Will backport by merging ed87465 (builtin-merge.c: call + exclude_cmds() correctly., 2009-11-25). + + * "git format-patch revisions... -- path" issued an incorrect error + message that suggested to use "--" on the command line when path + does not exist in the current work tree (it is a separate matter if + it makes sense to limit format-patch with pathspecs like that + without using the --full-diff option). Will backport by merging + 7e93d3b (format-patch: add test for parsing of "--", 2009-11-26). + + * "git shortlog" did not honor the "encoding" header embedded in the + commit object like "git log" did. Will backport by merging 79f7ca0 + (shortlog: respect commit encoding, 2009-11-25). + --- exec >/var/tmp/1 echo O=$(git describe master) -O=v1.6.5.3-337-gf341feb +O=v1.6.6-rc0-119-gc0ecb07 git shortlog --no-merges $O..master --not maint diff --git a/Documentation/config.txt b/Documentation/config.txt index 155e925fba..a1e36d7e42 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -126,12 +126,20 @@ advice.*:: Directions on how to stage/unstage/add shown in the output of linkgit:git-status[1] and the template shown when writing commit messages. Default: true. + commitBeforeMerge:: + Advice shown when linkgit:git-merge[1] refuses to + merge to avoid overwritting local changes. + Default: true. -- core.fileMode:: If false, the executable bit differences between the index and the working copy are ignored; useful on broken filesystems like FAT. - See linkgit:git-update-index[1]. True by default. + See linkgit:git-update-index[1]. ++ +The default is true, except linkgit:git-clone[1] or linkgit:git-init[1] +will probe and set core.fileMode false if appropriate when the +repository is created. core.ignoreCygwinFSTricks:: This option is only used by Cygwin implementation of Git. If false, @@ -144,6 +152,18 @@ core.ignoreCygwinFSTricks:: is true, in which case ignoreCygwinFSTricks is ignored as Cygwin's POSIX emulation is required to support core.filemode. +core.ignorecase:: + If true, this option enables various workarounds to enable + git to work better on filesystems that are not case sensitive, + like FAT. For example, if a directory listing finds + "makefile" when git expects "Makefile", git will assume + it is really the same file, and continue to remember it as + "Makefile". ++ +The default is false, except linkgit:git-clone[1] or linkgit:git-init[1] +will probe and set core.ignorecase true if appropriate when the repository +is created. + core.trustctime:: If false, the ctime differences between the index and the working copy are ignored; useful when the inode change time @@ -224,7 +244,11 @@ core.symlinks:: contain the link text. linkgit:git-update-index[1] and linkgit:git-add[1] will not change the recorded type to regular file. Useful on filesystems like FAT that do not support - symbolic links. True by default. + symbolic links. ++ +The default is true, except linkgit:git-clone[1] or linkgit:git-init[1] +will probe and set core.symlinks false if appropriate when the repository +is created. core.gitProxy:: A "proxy command" to execute (as 'command host port') instead @@ -611,10 +635,10 @@ color.diff.:: Use customized color for diff colorization. `` specifies which part of the patch to use the specified color, and is one of `plain` (context text), `meta` (metainformation), `frag` - (hunk header), `old` (removed lines), `new` (added lines), - `commit` (commit headers), or `whitespace` (highlighting - whitespace errors). The values of these variables may be specified as - in color.branch.. + (hunk header), 'func' (function in hunk header), `old` (removed lines), + `new` (added lines), `commit` (commit headers), or `whitespace` + (highlighting whitespace errors). The values of these variables may be + specified as in color.branch.. color.grep:: When set to `always`, always highlight matches. When `false` (or @@ -1417,7 +1441,13 @@ remote..mirror:: remote..skipDefaultUpdate:: If true, this remote will be skipped by default when updating - using the update subcommand of linkgit:git-remote[1]. + using linkgit:git-fetch[1] or the `update` subcommand of + linkgit:git-remote[1]. + +remote..skipFetchAll:: + If true, this remote will be skipped by default when updating + using linkgit:git-fetch[1] or the `update` subcommand of + linkgit:git-remote[1]. remote..receivepack:: The default program to execute on the remote side when pushing. See diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt index 2b37193a37..8707d0e740 100644 --- a/Documentation/diff-options.txt +++ b/Documentation/diff-options.txt @@ -85,10 +85,23 @@ ifndef::git-format-patch[] endif::git-format-patch[] ifndef::git-format-patch[] + -z:: - NUL-line termination on output. This affects the `--raw` - output field terminator. Also output from commands such - as `git-log` will be delimited with NUL between commits. +ifdef::git-log[] + Separate the commits with NULs instead of with new newlines. ++ +Also, when `--raw` or `--numstat` has been given, do not munge +pathnames and use NULs as output field terminators. +endif::git-log[] +ifndef::git-log[] + When `--raw` or `--numstat` has been given, do not munge + pathnames and use NULs as output field terminators. +endif::git-log[] ++ +Without this option, each pathname output will have TAB, LF, double quotes, +and backslash characters replaced with `\t`, `\n`, `\"`, and `\\`, +respectively, and the pathname will be enclosed in double quotes if +any of those replacements occurred. --name-only:: Show only names of changed files. diff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt index 28868747da..ab6419fe6e 100644 --- a/Documentation/fetch-options.txt +++ b/Documentation/fetch-options.txt @@ -1,3 +1,6 @@ +--all:: + Fetch all remotes. + -a:: --append:: Append ref names and object names of fetched refs to the @@ -9,6 +12,11 @@ `git clone` with `--depth=` option (see linkgit:git-clone[1]) by the specified number of commits. +ifndef::git-pull[] +--dry-run:: + Show what would be done, without making any changes. +endif::git-pull[] + -f:: --force:: When 'git-fetch' is used with `:` @@ -21,6 +29,16 @@ --keep:: Keep downloaded pack. +ifndef::git-pull[] +--multiple:: + Allow several and arguments to be + specified. No s may be specified. + +--prune:: + After fetching, remove any remote tracking branches which + no longer exist on the remote. +endif::git-pull[] + ifdef::git-pull[] --no-tags:: endif::git-pull[] diff --git a/Documentation/git-apply.txt b/Documentation/git-apply.txt index 5ee8c91f2d..c2528a7654 100644 --- a/Documentation/git-apply.txt +++ b/Documentation/git-apply.txt @@ -3,7 +3,7 @@ git-apply(1) NAME ---- -git-apply - Apply a patch on a git index file and/or a working tree +git-apply - Apply a patch to files and/or to the index SYNOPSIS @@ -20,8 +20,11 @@ SYNOPSIS DESCRIPTION ----------- -Reads supplied 'diff' output and applies it on a git index file -and a work tree. +Reads the supplied diff output (i.e. "a patch") and applies it to files. +With the `--index` option the patch is also applied to the index, and +with the `--cache` option the patch is only applied to the index. +Without these options, the command applies the patch only to files, +and does not require them to be in a git repository. OPTIONS ------- @@ -34,7 +37,7 @@ OPTIONS input. Turns off "apply". --numstat:: - Similar to \--stat, but shows the number of added and + Similar to `--stat`, but shows the number of added and deleted lines in decimal notation and the pathname without abbreviation, to make it more machine friendly. For binary files, outputs two `-` instead of saying @@ -48,22 +51,22 @@ OPTIONS --check:: Instead of applying the patch, see if the patch is - applicable to the current work tree and/or the index + applicable to the current working tree and/or the index file and detects errors. Turns off "apply". --index:: - When --check is in effect, or when applying the patch + When `--check` is in effect, or when applying the patch (which is the default when none of the options that disables it is in effect), make sure the patch is applicable to what the current index file records. If - the file to be patched in the work tree is not + the file to be patched in the working tree is not up-to-date, it is flagged as an error. This flag also causes the index file to be updated. --cached:: Apply a patch without touching the working tree. Instead take the cached data, apply the patch, and store the result in the index - without using the working tree. This implies '--index'. + without using the working tree. This implies `--index`. --build-fake-ancestor=:: Newer 'git-diff' output has embedded 'index information' @@ -87,11 +90,13 @@ the information is read from the current index instead. rejected hunks in corresponding *.rej files. -z:: - When showing the index information, do not munge paths, - but use NUL terminated machine readable format. Without - this flag, the pathnames output will have TAB, LF, and - backslash characters replaced with `\t`, `\n`, and `\\`, - respectively. + When `--numstat` has been given, do not munge pathnames, + but use a NUL-terminated machine-readable format. ++ +Without this option, each pathname output will have TAB, LF, double quotes, +and backslash characters replaced with `\t`, `\n`, `\"`, and `\\`, +respectively, and the pathname will be enclosed in double quotes if +any of those replacements occurred. -p:: Remove leading slashes from traditional diff paths. The @@ -107,8 +112,8 @@ the information is read from the current index instead. By default, 'git-apply' expects that the patch being applied is a unified diff with at least one line of context. This provides good safety measures, but breaks down when - applying a diff generated with --unified=0. To bypass these - checks use '--unidiff-zero'. + applying a diff generated with `--unified=0`. To bypass these + checks use `--unidiff-zero`. + Note, for the reasons stated above usage of context-free patches is discouraged. @@ -144,7 +149,7 @@ discouraged. be useful when importing patchsets, where you want to include certain files or directories. + -When --exclude and --include patterns are used, they are examined in the +When `--exclude` and `--include` patterns are used, they are examined in the order they appear on the command line, and the first match determines if a patch to each path is used. A patch to a path that does not match any include/exclude pattern is used by default if there is no include pattern @@ -227,13 +232,13 @@ Submodules If the patch contains any changes to submodules then 'git-apply' treats these changes as follows. -If --index is specified (explicitly or implicitly), then the submodule +If `--index` is specified (explicitly or implicitly), then the submodule commits must match the index exactly for the patch to apply. If any of the submodules are checked-out, then these check-outs are completely ignored, i.e., they are not required to be up-to-date or clean and they are not updated. -If --index is not specified, then the submodule commits in the patch +If `--index` is not specified, then the submodule commits in the patch are ignored and only the absence or presence of the corresponding subdirectory is checked and (if possible) updated. diff --git a/Documentation/git-bisect-lk2009.txt b/Documentation/git-bisect-lk2009.txt new file mode 100644 index 0000000000..6b7b2e5497 --- /dev/null +++ b/Documentation/git-bisect-lk2009.txt @@ -0,0 +1,1358 @@ +Fighting regressions with git bisect +==================================== +:Author: Christian Couder +:Email: chriscool@tuxfamily.org +:Date: 2009/11/08 + +Abstract +-------- + +"git bisect" enables software users and developers to easily find the +commit that introduced a regression. We show why it is important to +have good tools to fight regressions. We describe how "git bisect" +works from the outside and the algorithms it uses inside. Then we +explain how to take advantage of "git bisect" to improve current +practices. And we discuss how "git bisect" could improve in the +future. + + +Introduction to "git bisect" +---------------------------- + +Git is a Distributed Version Control system (DVCS) created by Linus +Torvalds and maintained by Junio Hamano. + +In Git like in many other Version Control Systems (VCS), the different +states of the data that is managed by the system are called +commits. And, as VCS are mostly used to manage software source code, +sometimes "interesting" changes of behavior in the software are +introduced in some commits. + +In fact people are specially interested in commits that introduce a +"bad" behavior, called a bug or a regression. They are interested in +these commits because a commit (hopefully) contains a very small set +of source code changes. And it's much easier to understand and +properly fix a problem when you only need to check a very small set of +changes, than when you don't know where look in the first place. + +So to help people find commits that introduce a "bad" behavior, the +"git bisect" set of commands was invented. And it follows of course +that in "git bisect" parlance, commits where the "interesting +behavior" is present are called "bad" commits, while other commits are +called "good" commits. And a commit that introduce the behavior we are +interested in is called a "first bad commit". Note that there could be +more than one "first bad commit" in the commit space we are searching. + +So "git bisect" is designed to help find a "first bad commit". And to +be as efficient as possible, it tries to perform a binary search. + + +Fighting regressions overview +----------------------------- + +Regressions: a big problem +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Regressions are a big problem in the software industry. But it's +difficult to put some real numbers behind that claim. + +There are some numbers about bugs in general, like a NIST study in +2002 <<1>> that said: + +_____________ +Software bugs, or errors, are so prevalent and so detrimental that +they cost the U.S. economy an estimated $59.5 billion annually, or +about 0.6 percent of the gross domestic product, according to a newly +released study commissioned by the Department of Commerce's National +Institute of Standards and Technology (NIST). At the national level, +over half of the costs are borne by software users and the remainder +by software developers/vendors. The study also found that, although +all errors cannot be removed, more than a third of these costs, or an +estimated $22.2 billion, could be eliminated by an improved testing +infrastructure that enables earlier and more effective identification +and removal of software defects. These are the savings associated with +finding an increased percentage (but not 100 percent) of errors closer +to the development stages in which they are introduced. Currently, +over half of all errors are not found until "downstream" in the +development process or during post-sale software use. +_____________ + +And then: + +_____________ +Software developers already spend approximately 80 percent of +development costs on identifying and correcting defects, and yet few +products of any type other than software are shipped with such high +levels of errors. +_____________ + +Eventually the conclusion started with: + +_____________ +The path to higher software quality is significantly improved software +testing. +_____________ + +There are other estimates saying that 80% of the cost related to +software is about maintenance <<2>>. + +Though, according to Wikipedia <<3>>: + +_____________ +A common perception of maintenance is that it is merely fixing +bugs. However, studies and surveys over the years have indicated that +the majority, over 80%, of the maintenance effort is used for +non-corrective actions (Pigosky 1997). This perception is perpetuated +by users submitting problem reports that in reality are functionality +enhancements to the system. +_____________ + +But we can guess that improving on existing software is very costly +because you have to watch out for regressions. At least this would +make the above studies consistent among themselves. + +Of course some kind of software is developed, then used during some +time without being improved on much, and then finally thrown away. In +this case, of course, regressions may not be a big problem. But on the +other hand, there is a lot of big software that is continually +developed and maintained during years or even tens of years by a lot +of people. And as there are often many people who depend (sometimes +critically) on such software, regressions are a really big problem. + +One such software is the linux kernel. And if we look at the linux +kernel, we can see that a lot of time and effort is spent to fight +regressions. The release cycle start with a 2 weeks long merge +window. Then the first release candidate (rc) version is tagged. And +after that about 7 or 8 more rc versions will appear with around one +week between each of them, before the final release. + +The time between the first rc release and the final release is +supposed to be used to test rc versions and fight bugs and especially +regressions. And this time is more than 80% of the release cycle +time. But this is not the end of the fight yet, as of course it +continues after the release. + +And then this is what Ingo Molnar (a well known linux kernel +developer) says about his use of git bisect: + +_____________ +I most actively use it during the merge window (when a lot of trees +get merged upstream and when the influx of bugs is the highest) - and +yes, there have been cases that i used it multiple times a day. My +average is roughly once a day. +_____________ + +So regressions are fought all the time by developers, and indeed it is +well known that bugs should be fixed as soon as possible, so as soon +as they are found. That's why it is interesting to have good tools for +this purpose. + +Other tools to fight regressions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +So what are the tools used to fight regressions? They are nearly the +same as those used to fight regular bugs. The only specific tools are +test suites and tools similar as "git bisect". + +Test suites are very nice. But when they are used alone, they are +supposed to be used so that all the tests are checked after each +commit. This means that they are not very efficient, because many +tests are run for no interesting result, and they suffer from +combinational explosion. + +In fact the problem is that big software often has many different +configuration options and that each test case should pass for each +configuration after each commit. So if you have for each release: N +configurations, M commits and T test cases, you should perform: + +------------- +N * M * T tests +------------- + +where N, M and T are all growing with the size your software. + +So very soon it will not be possible to completely test everything. + +And if some bugs slip through your test suite, then you can add a test +to your test suite. But if you want to use your new improved test +suite to find where the bug slipped in, then you will either have to +emulate a bisection process or you will perhaps bluntly test each +commit backward starting from the "bad" commit you have which may be +very wasteful. + +"git bisect" overview +--------------------- + +Starting a bisection +~~~~~~~~~~~~~~~~~~~~ + +The first "git bisect" subcommand to use is "git bisect start" to +start the search. Then bounds must be set to limit the commit +space. This is done usually by giving one "bad" and at least one +"good" commit. They can be passed in the initial call to "git bisect +start" like this: + +------------- +$ git bisect start [BAD [GOOD...]] +------------- + +or they can be set using: + +------------- +$ git bisect bad [COMMIT] +------------- + +and: + +------------- +$ git bisect good [COMMIT...] +------------- + +where BAD, GOOD and COMMIT are all names that can be resolved to a +commit. + +Then "git bisect" will checkout a commit of its choosing and ask the +user to test it, like this: + +------------- +$ git bisect start v2.6.27 v2.6.25 +Bisecting: 10928 revisions left to test after this (roughly 14 steps) +[2ec65f8b89ea003c27ff7723525a2ee335a2b393] x86: clean up using max_low_pfn on 32-bit +------------- + +Note that the example that we will use is really a toy example, we +will be looking for the first commit that has a version like +"2.6.26-something", that is the commit that has a "SUBLEVEL = 26" line +in the top level Makefile. This is a toy example because there are +better ways to find this commit with git than using "git bisect" (for +example "git blame" or "git log -S"). + +Driving a bisection manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At this point there are basically 2 ways to drive the search. It can +be driven manually by the user or it can be driven automatically by a +script or a command. + +If the user is driving it, then at each step of the search, the user +will have to test the current commit and say if it is "good" or "bad" +using the "git bisect good" or "git bisect bad" commands respectively +that have been described above. For example: + +------------- +$ git bisect bad +Bisecting: 5480 revisions left to test after this (roughly 13 steps) +[66c0b394f08fd89236515c1c84485ea712a157be] KVM: kill file->f_count abuse in kvm +------------- + +And after a few more steps like that, "git bisect" will eventually +find a first bad commit: + +------------- +$ git bisect bad +2ddcca36c8bcfa251724fe342c8327451988be0d is the first bad commit +commit 2ddcca36c8bcfa251724fe342c8327451988be0d +Author: Linus Torvalds +Date: Sat May 3 11:59:44 2008 -0700 + + Linux 2.6.26-rc1 + +:100644 100644 5cf8258195331a4dbdddff08b8d68642638eea57 4492984efc09ab72ff6219a7bc21fb6a957c4cd5 M Makefile +------------- + +At this point we can see what the commit does, check it out (if it's +not already checked out) or tinker with it, for example: + +------------- +$ git show HEAD +commit 2ddcca36c8bcfa251724fe342c8327451988be0d +Author: Linus Torvalds +Date: Sat May 3 11:59:44 2008 -0700 + + Linux 2.6.26-rc1 + +diff --git a/Makefile b/Makefile +index 5cf8258..4492984 100644 +--- a/Makefile ++++ b/Makefile +@@ -1,7 +1,7 @@ + VERSION = 2 + PATCHLEVEL = 6 +-SUBLEVEL = 25 +-EXTRAVERSION = ++SUBLEVEL = 26 ++EXTRAVERSION = -rc1 + NAME = Funky Weasel is Jiggy wit it + + # *DOCUMENTATION* +------------- + +And when we are finished we can use "git bisect reset" to go back to +the branch we were in before we started bisecting: + +------------- +$ git bisect reset +Checking out files: 100% (21549/21549), done. +Previous HEAD position was 2ddcca3... Linux 2.6.26-rc1 +Switched to branch 'master' +------------- + +Driving a bisection automatically +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The other way to drive the bisection process is to tell "git bisect" +to launch a script or command at each bisection step to know if the +current commit is "good" or "bad". To do that, we use the "git bisect +run" command. For example: + +------------- +$ git bisect start v2.6.27 v2.6.25 +Bisecting: 10928 revisions left to test after this (roughly 14 steps) +[2ec65f8b89ea003c27ff7723525a2ee335a2b393] x86: clean up using max_low_pfn on 32-bit +$ +$ git bisect run grep '^SUBLEVEL = 25' Makefile +running grep ^SUBLEVEL = 25 Makefile +Bisecting: 5480 revisions left to test after this (roughly 13 steps) +[66c0b394f08fd89236515c1c84485ea712a157be] KVM: kill file->f_count abuse in kvm +running grep ^SUBLEVEL = 25 Makefile +SUBLEVEL = 25 +Bisecting: 2740 revisions left to test after this (roughly 12 steps) +[671294719628f1671faefd4882764886f8ad08cb] V4L/DVB(7879): Adding cx18 Support for mxl5005s +... +... +running grep ^SUBLEVEL = 25 Makefile +Bisecting: 0 revisions left to test after this (roughly 0 steps) +[2ddcca36c8bcfa251724fe342c8327451988be0d] Linux 2.6.26-rc1 +running grep ^SUBLEVEL = 25 Makefile +2ddcca36c8bcfa251724fe342c8327451988be0d is the first bad commit +commit 2ddcca36c8bcfa251724fe342c8327451988be0d +Author: Linus Torvalds +Date: Sat May 3 11:59:44 2008 -0700 + + Linux 2.6.26-rc1 + +:100644 100644 5cf8258195331a4dbdddff08b8d68642638eea57 4492984efc09ab72ff6219a7bc21fb6a957c4cd5 M Makefile +bisect run success +------------- + +In this example, we passed "grep '^SUBLEVEL = 25' Makefile" as +parameter to "git bisect run". This means that at each step, the grep +command we passed will be launched. And if it exits with code 0 (that +means success) then git bisect will mark the current state as +"good". If it exits with code 1 (or any code between 1 and 127 +included, except the special code 125), then the current state will be +marked as "bad". + +Exit code between 128 and 255 are special to "git bisect run". They +make it stop immediately the bisection process. This is useful for +example if the command passed takes too long to complete, because you +can kill it with a signal and it will stop the bisection process. + +It can also be useful in scripts passed to "git bisect run" to "exit +255" if some very abnormal situation is detected. + +Avoiding untestable commits +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes it happens that the current state cannot be tested, for +example if it does not compile because there was a bug preventing it +at that time. This is what the special exit code 125 is for. It tells +"git bisect run" that the current commit should be marked as +untestable and that another one should be chosen and checked out. + +If the bisection process is driven manually, you can use "git bisect +skip" to do the same thing. (In fact the special exit code 125 makes +"git bisect run" use "git bisect skip" in the background.) + +Or if you want more control, you can inspect the current state using +for example "git bisect visualize". It will launch gitk (or "git log" +if the DISPLAY environment variable is not set) to help you find a +better bisection point. + +Either way, if you have a string of untestable commits, it might +happen that the regression you are looking for has been introduced by +one of these untestable commits. In this case it's not possible to +tell for sure which commit introduced the regression. + +So if you used "git bisect skip" (or the run script exited with +special code 125) you could get a result like this: + +------------- +There are only 'skip'ped commits left to test. +The first bad commit could be any of: +15722f2fa328eaba97022898a305ffc8172db6b1 +78e86cf3e850bd755bb71831f42e200626fbd1e0 +e15b73ad3db9b48d7d1ade32f8cd23a751fe0ace +070eab2303024706f2924822bfec8b9847e4ac1b +We cannot bisect more! +------------- + +Saving a log and replaying it +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to show other people your bisection process, you can get a +log using for example: + +------------- +$ git bisect log > bisect_log.txt +------------- + +And it is possible to replay it using: + +------------- +$ git bisect replay bisect_log.txt +------------- + + +"git bisect" details +-------------------- + +Bisection algorithm +~~~~~~~~~~~~~~~~~~~ + +As the Git commits form a directed acyclic graph (DAG), finding the +best bisection commit to test at each step is not so simple. Anyway +Linus found and implemented a "truly stupid" algorithm, later improved +by Junio Hamano, that works quite well. + +So the algorithm used by "git bisect" to find the best bisection +commit when there are no skipped commits is the following: + +1) keep only the commits that: + +a) are ancestor of the "bad" commit (including the "bad" commit itself), +b) are not ancestor of a "good" commit (excluding the "good" commits). + +This means that we get rid of the uninteresting commits in the DAG. + +For example if we start with a graph like this: + +------------- +G-Y-G-W-W-W-X-X-X-X + \ / + W-W-B + / +Y---G-W---W + \ / \ +Y-Y X-X-X-X + +-> time goes this way -> +------------- + +where B is the "bad" commit, "G" are "good" commits and W, X, and Y +are other commits, we will get the following graph after this first +step: + +------------- +W-W-W + \ + W-W-B + / +W---W +------------- + +So only the W and B commits will be kept. Because commits X and Y will +have been removed by rules a) and b) respectively, and because commits +G are removed by rule b) too. + +Note for git users, that it is equivalent as keeping only the commit +given by: + +------------- +git rev-list BAD --not GOOD1 GOOD2... +------------- + +Also note that we don't require the commits that are kept to be +descendants of a "good" commit. So in the following example, commits W +and Z will be kept: + +------------- +G-W-W-W-B + / +Z-Z +------------- + +2) starting from the "good" ends of the graph, associate to each +commit the number of ancestors it has plus one + +For example with the following graph where H is the "bad" commit and A +and D are some parents of some "good" commits: + +------------- +A-B-C + \ + F-G-H + / +D---E +------------- + +this will give: + +------------- +1 2 3 +A-B-C + \6 7 8 + F-G-H +1 2/ +D---E +------------- + +3) associate to each commit: min(X, N - X) + +where X is the value associated to the commit in step 2) and N is the +total number of commits in the graph. + +In the above example we have N = 8, so this will give: + +------------- +1 2 3 +A-B-C + \2 1 0 + F-G-H +1 2/ +D---E +------------- + +4) the best bisection point is the commit with the highest associated +number + +So in the above example the best bisection point is commit C. + +5) note that some shortcuts are implemented to speed up the algorithm + +As we know N from the beginning, we know that min(X, N - X) can't be +greater than N/2. So during steps 2) and 3), if we would associate N/2 +to a commit, then we know this is the best bisection point. So in this +case we can just stop processing any other commit and return the +current commit. + +Bisection algorithm debugging +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For any commit graph, you can see the number associated with each +commit using "git rev-list --bisect-all". + +For example, for the above graph, a command like: + +------------- +$ git rev-list --bisect-all BAD --not GOOD1 GOOD2 +------------- + +would output something like: + +------------- +e15b73ad3db9b48d7d1ade32f8cd23a751fe0ace (dist=3) +15722f2fa328eaba97022898a305ffc8172db6b1 (dist=2) +78e86cf3e850bd755bb71831f42e200626fbd1e0 (dist=2) +a1939d9a142de972094af4dde9a544e577ddef0e (dist=2) +070eab2303024706f2924822bfec8b9847e4ac1b (dist=1) +a3864d4f32a3bf5ed177ddef598490a08760b70d (dist=1) +a41baa717dd74f1180abf55e9341bc7a0bb9d556 (dist=1) +9e622a6dad403b71c40979743bb9d5be17b16bd6 (dist=0) +------------- + +Bisection algorithm discussed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First let's define "best bisection point". We will say that a commit X +is a best bisection point or a best bisection commit if knowing its +state ("good" or "bad") gives as much information as possible whether +the state of the commit happens to be "good" or "bad". + +This means that the best bisection commits are the commits where the +following function is maximum: + +------------- +f(X) = min(information_if_good(X), information_if_bad(X)) +------------- + +where information_if_good(X) is the information we get if X is good +and information_if_bad(X) is the information we get if X is bad. + +Now we will suppose that there is only one "first bad commit". This +means that all its descendants are "bad" and all the other commits are +"good". And we will suppose that all commits have an equal probability +of being good or bad, or of being the first bad commit, so knowing the +state of c commits gives always the same amount of information +wherever these c commits are on the graph and whatever c is. (So we +suppose that these commits being for example on a branch or near a +good or a bad commit does not give more or less information). + +Let's also suppose that we have a cleaned up graph like one after step +1) in the bisection algorithm above. This means that we can measure +the information we get in terms of number of commit we can remove from +the graph.. + +And let's take a commit X in the graph. + +If X is found to be "good", then we know that its ancestors are all +"good", so we want to say that: + +------------- +information_if_good(X) = number_of_ancestors(X) (TRUE) +------------- + +And this is true because at step 1) b) we remove the ancestors of the +"good" commits. + +If X is found to be "bad", then we know that its descendants are all +"bad", so we want to say that: + +------------- +information_if_bad(X) = number_of_descendants(X) (WRONG) +------------- + +But this is wrong because at step 1) a) we keep only the ancestors of +the bad commit. So we get more information when a commit is marked as +"bad", because we also know that the ancestors of the previous "bad" +commit that are not ancestors of the new "bad" commit are not the +first bad commit. We don't know if they are good or bad, but we know +that they are not the first bad commit because they are not ancestor +of the new "bad" commit. + +So when a commit is marked as "bad" we know we can remove all the +commits in the graph except those that are ancestors of the new "bad" +commit. This means that: + +------------- +information_if_bad(X) = N - number_of_ancestors(X) (TRUE) +------------- + +where N is the number of commits in the (cleaned up) graph. + +So in the end this means that to find the best bisection commits we +should maximize the function: + +------------- +f(X) = min(number_of_ancestors(X), N - number_of_ancestors(X)) +------------- + +And this is nice because at step 2) we compute number_of_ancestors(X) +and so at step 3) we compute f(X). + +Let's take the following graph as an example: + +------------- + G-H-I-J + / \ +A-B-C-D-E-F O + \ / + K-L-M-N +------------- + +If we compute the following non optimal function on it: + +------------- +g(X) = min(number_of_ancestors(X), number_of_descendants(X)) +------------- + +we get: + +------------- + 4 3 2 1 + G-H-I-J +1 2 3 4 5 6/ \0 +A-B-C-D-E-F O + \ / + K-L-M-N + 4 3 2 1 +------------- + +but with the algorithm used by git bisect we get: + +------------- + 7 7 6 5 + G-H-I-J +1 2 3 4 5 6/ \0 +A-B-C-D-E-F O + \ / + K-L-M-N + 7 7 6 5 +------------- + +So we chose G, H, K or L as the best bisection point, which is better +than F. Because if for example L is bad, then we will know not only +that L, M and N are bad but also that G, H, I and J are not the first +bad commit (since we suppose that there is only one first bad commit +and it must be an ancestor of L). + +So the current algorithm seems to be the best possible given what we +initially supposed. + +Skip algorithm +~~~~~~~~~~~~~~ + +When some commits have been skipped (using "git bisect skip"), then +the bisection algorithm is the same for step 1) to 3). But then we use +roughly the following steps: + +6) sort the commit by decreasing associated value + +7) if the first commit has not been skipped, we can return it and stop +here + +8) otherwise filter out all the skipped commits in the sorted list + +9) use a pseudo random number generator (PRNG) to generate a random +number between 0 and 1 + +10) multiply this random number with its square root to bias it toward +0 + +11) multiply the result by the number of commits in the filtered list +to get an index into this list + +12) return the commit at the computed index + +Skip algorithm discussed +~~~~~~~~~~~~~~~~~~~~~~~~ + +After step 7) (in the skip algorithm), we could check if the second +commit has been skipped and return it if it is not the case. And in +fact that was the algorithm we used from when "git bisect skip" was +developed in git version 1.5.4 (released on February 1st 2008) until +git version 1.6.4 (released July 29th 2009). + +But Ingo Molnar and H. Peter Anvin (another well known linux kernel +developer) both complained that sometimes the best bisection points +all happened to be in an area where all the commits are +untestable. And in this case the user was asked to test many +untestable commits, which could be very inefficient. + +Indeed untestable commits are often untestable because a breakage was +introduced at one time, and that breakage was fixed only after many +other commits were introduced. + +This breakage is of course most of the time unrelated to the breakage +we are trying to locate in the commit graph. But it prevents us to +know if the interesting "bad behavior" is present or not. + +So it is a fact that commits near an untestable commit have a high +probability of being untestable themselves. And the best bisection +commits are often found together too (due to the bisection algorithm). + +This is why it is a bad idea to just chose the next best unskipped +bisection commit when the first one has been skipped. + +We found that most commits on the graph may give quite a lot of +information when they are tested. And the commits that will not on +average give a lot of information are the one near the good and bad +commits. + +So using a PRNG with a bias to favor commits away from the good and +bad commits looked like a good choice. + +One obvious improvement to this algorithm would be to look for a +commit that has an associated value near the one of the best bisection +commit, and that is on another branch, before using the PRNG. Because +if such a commit exists, then it is not very likely to be untestable +too, so it will probably give more information than a nearly randomly +chosen one. + +Checking merge bases +~~~~~~~~~~~~~~~~~~~~ + +There is another tweak in the bisection algorithm that has not been +described in the "bisection algorithm" above. + +We supposed in the previous examples that the "good" commits were +ancestors of the "bad" commit. But this is not a requirement of "git +bisect". + +Of course the "bad" commit cannot be an ancestor of a "good" commit, +because the ancestors of the good commits are supposed to be +"good". And all the "good" commits must be related to the bad commit. +They cannot be on a branch that has no link with the branch of the +"bad" commit. But it is possible for a good commit to be related to a +bad commit and yet not be neither one of its ancestor nor one of its +descendants. + +For example, there can be a "main" branch, and a "dev" branch that was +forked of the main branch at a commit named "D" like this: + +------------- +A-B-C-D-E-F-G <--main + \ + H-I-J <--dev +------------- + +The commit "D" is called a "merge base" for branch "main" and "dev" +because it's the best common ancestor for these branches for a merge. + +Now let's suppose that commit J is bad and commit G is good and that +we apply the bisection algorithm like it has been previously +described. + +As described in step 1) b) of the bisection algorithm, we remove all +the ancestors of the good commits because they are supposed to be good +too. + +So we would be left with only: + +------------- +H-I-J +------------- + +But what happens if the first bad commit is "B" and if it has been +fixed in the "main" branch by commit "F"? + +The result of such a bisection would be that we would find that H is +the first bad commit, when in fact it's B. So that would be wrong! + +And yes it's can happen in practice that people working on one branch +are not aware that people working on another branch fixed a bug! It +could also happen that F fixed more than one bug or that it is a +revert of some big development effort that was not ready to be +released. + +In fact development teams often maintain both a development branch and +a maintenance branch, and it would be quite easy for them if "git +bisect" just worked when they want to bisect a regression on the +development branch that is not on the maintenance branch. They should +be able to start bisecting using: + +------------- +$ git bisect start dev main +------------- + +To enable that additional nice feature, when a bisection is started +and when some good commits are not ancestors of the bad commit, we +first compute the merge bases between the bad and the good commits and +we chose these merge bases as the first commits that will be checked +out and tested. + +If it happens that one merge base is bad, then the bisection process +is stopped with a message like: + +------------- +The merge base BBBBBB is bad. +This means the bug has been fixed between BBBBBB and [GGGGGG,...]. +------------- + +where BBBBBB is the sha1 hash of the bad merge base and [GGGGGG,...] +is a comma separated list of the sha1 of the good commits. + +If some of the merge bases are skipped, then the bisection process +continues, but the following message is printed for each skipped merge +base: + +------------- +Warning: the merge base between BBBBBB and [GGGGGG,...] must be skipped. +So we cannot be sure the first bad commit is between MMMMMM and BBBBBB. +We continue anyway. +------------- + +where BBBBBB is the sha1 hash of the bad commit, MMMMMM is the sha1 +hash of the merge base that is skipped and [GGGGGG,...] is a comma +separated list of the sha1 of the good commits. + +So if there is no bad merge base, the bisection process continues as +usual after this step. + +Best bisecting practices +------------------------ + +Using test suites and git bisect together +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you both have a test suite and use git bisect, then it becomes less +important to check that all tests pass after each commit. Though of +course it is probably a good idea to have some checks to avoid +breaking too many things because it could make bisecting other bugs +more difficult. + +You can focus your efforts to check at a few points (for example rc +and beta releases) that all the T test cases pass for all the N +configurations. And when some tests don't pass you can use "git +bisect" (or better "git bisect run"). So you should perform roughly: + +------------- +c * N * T + b * M * log2(M) tests +------------- + +where c is the number of rounds of test (so a small constant) and b is +the ratio of bug per commit (hopefully a small constant too). + +So of course it's much better as it's O(N \* T) vs O(N \* T \* M) if +you would test everything after each commit. + +This means that test suites are good to prevent some bugs from being +committed and they are also quite good to tell you that you have some +bugs. But they are not so good to tell you where some bugs have been +introduced. To tell you that efficiently, git bisect is needed. + +The other nice thing with test suites, is that when you have one, you +already know how to test for bad behavior. So you can use this +knowledge to create a new test case for "git bisect" when it appears +that there is a regression. So it will be easier to bisect the bug and +fix it. And then you can add the test case you just created to your +test suite. + +So if you know how to create test cases and how to bisect, you will be +subject to a virtuous circle: + +more tests => easier to create tests => easier to bisect => more tests + +So test suites and "git bisect" are complementary tools that are very +powerful and efficient when used together. + +Bisecting build failures +~~~~~~~~~~~~~~~~~~~~~~~~ + +You can very easily automatically bisect broken builds using something +like: + +------------- +$ git bisect start BAD GOOD +$ git bisect run make +------------- + +Passing sh -c "some commands" to "git bisect run" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For example: + +------------- +$ git bisect run sh -c "make || exit 125; ./my_app | grep 'good output'" +------------- + +On the other hand if you do this often, then it can be worth having +scripts to avoid too much typing. + +Finding performance regressions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here is an example script that comes slightly modified from a real +world script used by Junio Hamano <<4>>. + +This script can be passed to "git bisect run" to find the commit that +introduced a performance regression: + +------------- +#!/bin/sh + +# Build errors are not what I am interested in. +make my_app || exit 255 + +# We are checking if it stops in a reasonable amount of time, so +# let it run in the background... + +./my_app >log 2>&1 & + +# ... and grab its process ID. +pid=$! + +# ... and then wait for sufficiently long. +sleep $NORMAL_TIME + +# ... and then see if the process is still there. +if kill -0 $pid +then + # It is still running -- that is bad. + kill $pid; sleep 1; kill $pid; + exit 1 +else + # It has already finished (the $pid process was no more), + # and we are happy. + exit 0 +fi +------------- + +Following general best practices +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is obviously a good idea not to have commits with changes that +knowingly break things, even if some other commits later fix the +breakage. + +It is also a good idea when using any VCS to have only one small +logical change in each commit. + +The smaller the changes in your commit, the most effective "git +bisect" will be. And you will probably need "git bisect" less in the +first place, as small changes are easier to review even if they are +only reviewed by the commiter. + +Another good idea is to have good commit messages. They can be very +helpful to understand why some changes were made. + +These general best practices are very helpful if you bisect often. + +Avoiding bug prone merges +~~~~~~~~~~~~~~~~~~~~~~~~~ + +First merges by themselves can introduce some regressions even when +the merge needs no source code conflict resolution. This is because a +semantic change can happen in one branch while the other branch is not +aware of it. + +For example one branch can change the semantic of a function while the +other branch add more calls to the same function. + +This is made much worse if many files have to be fixed to resolve +conflicts. That's why such merges are called "evil merges". They can +make regressions very difficult to track down. It can even be +misleading to know the first bad commit if it happens to be such a +merge, because people might think that the bug comes from bad conflict +resolution when it comes from a semantic change in one branch. + +Anyway "git rebase" can be used to linearize history. This can be used +either to avoid merging in the first place. Or it can be used to +bisect on a linear history instead of the non linear one, as this +should give more information in case of a semantic change in one +branch. + +Merges can be also made simpler by using smaller branches or by using +many topic branches instead of only long version related branches. + +And testing can be done more often in special integration branches +like linux-next for the linux kernel. + +Adapting your work-flow +~~~~~~~~~~~~~~~~~~~~~~~ + +A special work-flow to process regressions can give great results. + +Here is an example of a work-flow used by Andreas Ericsson: + +* write, in the test suite, a test script that exposes the regression +* use "git bisect run" to find the commit that introduced it +* fix the bug that is often made obvious by the previous step +* commit both the fix and the test script (and if needed more tests) + +And here is what Andreas said about this work-flow <<5>>: + +_____________ +To give some hard figures, we used to have an average report-to-fix +cycle of 142.6 hours (according to our somewhat weird bug-tracker +which just measures wall-clock time). Since we moved to git, we've +lowered that to 16.2 hours. Primarily because we can stay on top of +the bug fixing now, and because everyone's jockeying to get to fix +bugs (we're quite proud of how lazy we are to let git find the bugs +for us). Each new release results in ~40% fewer bugs (almost certainly +due to how we now feel about writing tests). +_____________ + +Clearly this work-flow uses the virtuous circle between test suites +and "git bisect". In fact it makes it the standard procedure to deal +with regression. + +In other messages Andreas says that they also use the "best practices" +described above: small logical commits, topic branches, no evil +merge,... These practices all improve the bisectability of the commit +graph, by making it easier and more useful to bisect. + +So a good work-flow should be designed around the above points. That +is making bisecting easier, more useful and standard. + +Involving QA people and if possible end users +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One nice about "git bisect" is that it is not only a developer +tool. It can effectively be used by QA people or even end users (if +they have access to the source code or if they can get access to all +the builds). + +There was a discussion at one point on the linux kernel mailing list +of whether it was ok to always ask end user to bisect, and very good +points were made to support the point of view that it is ok. + +For example David Miller wrote <<6>>: + +_____________ +What people don't get is that this is a situation where the "end node +principle" applies. When you have limited resources (here: developers) +you don't push the bulk of the burden upon them. Instead you push +things out to the resource you have a lot of, the end nodes (here: +users), so that the situation actually scales. +_____________ + +This means that it is often "cheaper" if QA people or end users can do +it. + +What is interesting too is that end users that are reporting bugs (or +QA people that reproduced a bug) have access to the environment where +the bug happens. So they can often more easily reproduce a +regression. And if they can bisect, then more information will be +extracted from the environment where the bug happens, which means that +it will be easier to understand and then fix the bug. + +For open source projects it can be a good way to get more useful +contributions from end users, and to introduce them to QA and +development activities. + +Using complex scripts +~~~~~~~~~~~~~~~~~~~~~ + +In some cases like for kernel development it can be worth developing +complex scripts to be able to fully automate bisecting. + +Here is what Ingo Molnar says about that <<7>>: + +_____________ +i have a fully automated bootup-hang bisection script. It is based on +"git-bisect run". I run the script, it builds and boots kernels fully +automatically, and when the bootup fails (the script notices that via +the serial log, which it continuously watches - or via a timeout, if +the system does not come up within 10 minutes it's a "bad" kernel), +the script raises my attention via a beep and i power cycle the test +box. (yeah, i should make use of a managed power outlet to 100% +automate it) +_____________ + +Combining test suites, git bisect and other systems together +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We have seen that test suites an git bisect are very powerful when +used together. It can be even more powerful if you can combine them +with other systems. + +For example some test suites could be run automatically at night with +some unusual (or even random) configurations. And if a regression is +found by a test suite, then "git bisect" can be automatically +launched, and its result can be emailed to the author of the first bad +commit found by "git bisect", and perhaps other people too. And a new +entry in the bug tracking system could be automatically created too. + + +The future of bisecting +----------------------- + +"git replace" +~~~~~~~~~~~~~ + +We saw earlier that "git bisect skip" is now using a PRNG to try to +avoid areas in the commit graph where commits are untestable. The +problem is that sometimes the first bad commit will be in an +untestable area. + +To simplify the discussion we will suppose that the untestable area is +a simple string of commits and that it was created by a breakage +introduced by one commit (let's call it BBC for bisect breaking +commit) and later fixed by another one (let's call it BFC for bisect +fixing commit). + +For example: + +------------- +...-Y-BBC-X1-X2-X3-X4-X5-X6-BFC-Z-... +------------- + +where we know that Y is good and BFC is bad, and where BBC and X1 to +X6 are untestable. + +In this case if you are bisecting manually, what you can do is create +a special branch that starts just before the BBC. The first commit in +this branch should be the BBC with the BFC squashed into it. And the +other commits in the branch should be the commits between BBC and BFC +rebased on the first commit of the branch and then the commit after +BFC also rebased on. + +For example: + +------------- + (BBC+BFC)-X1'-X2'-X3'-X4'-X5'-X6'-Z' + / +...-Y-BBC-X1-X2-X3-X4-X5-X6-BFC-Z-... +------------- + +where commits quoted with ' have been rebased. + +You can easily create such a branch with Git using interactive rebase. + +For example using: + +------------- +$ git rebase -i Y Z +------------- + +and then moving BFC after BBC and squashing it. + +After that you can start bisecting as usual in the new branch and you +should eventually find the first bad commit. + +For example: + +------------- +$ git bisect start Z' Y +------------- + +If you are using "git bisect run", you can use the same manual fix up +as above, and then start another "git bisect run" in the special +branch. Or as the "git bisect" man page says, the script passed to +"git bisect run" can apply a patch before it compiles and test the +software <<8>>. The patch should turn a current untestable commits +into a testable one. So the testing will result in "good" or "bad" and +"git bisect" will be able to find the first bad commit. And the script +should not forget to remove the patch once the testing is done before +exiting from the script. + +(Note that instead of a patch you can use "git cherry-pick BFC" to +apply the fix, and in this case you should use "git reset --hard +HEAD^" to revert the cherry-pick after testing and before returning +from the script.) + +But the above ways to work around untestable areas are a little bit +clunky. Using special branches is nice because these branches can be +shared by developers like usual branches, but the risk is that people +will get many such branches. And it disrupts the normal "git bisect" +work-flow. So, if you want to use "git bisect run" completely +automatically, you have to add special code in your script to restart +bisection in the special branches. + +Anyway one can notice in the above special branch example that the Z' +and Z commits should point to the same source code state (the same +"tree" in git parlance). That's because Z' result from applying the +same changes as Z just in a slightly different order. + +So if we could just "replace" Z by Z' when we bisect, then we would +not need to add anything to a script. It would just work for anyone in +the project sharing the special branches and the replacements. + +With the example above that would give: + +------------- + (BBC+BFC)-X1'-X2'-X3'-X4'-X5'-X6'-Z'-... + / +...-Y-BBC-X1-X2-X3-X4-X5-X6-BFC-Z +------------- + +That's why the "git replace" command was created. Technically it +stores replacements "refs" in the "refs/replace/" hierarchy. These +"refs" are like branches (that are stored in "refs/heads/") or tags +(that are stored in "refs/tags"), and that means that they can +automatically be shared like branches or tags among developers. + +"git replace" is a very powerful mechanism. It can be used to fix +commits in already released history, for example to change the commit +message or the author. And it can also be used instead of git "grafts" +to link a repository with another old repository. + +In fact it's this last feature that "sold" it to the git community, so +it is now in the "master" branch of git's git repository and it should +be released in git 1.6.5 in October or November 2009. + +One problem with "git replace" is that currently it stores all the +replacements refs in "refs/replace/", but it would be perhaps better +if the replacement refs that are useful only for bisecting would be in +"refs/replace/bisect/". This way the replacement refs could be used +only for bisecting, while other refs directly in "refs/replace/" would +be used nearly all the time. + +Bisecting sporadic bugs +~~~~~~~~~~~~~~~~~~~~~~~ + +Another possible improvement to "git bisect" would be to optionally +add some redundancy to the tests performed so that it would be more +reliable when tracking sporadic bugs. + +This has been requested by some kernel developers because some bugs +called sporadic bugs do not appear in all the kernel builds because +they are very dependent on the compiler output. + +The idea is that every 3 test for example, "git bisect" could ask the +user to test a commit that has already been found to be "good" or +"bad" (because one of its descendants or one of its ancestors has been +found to be "good" or "bad" respectively). If it happens that a commit +has been previously incorrectly classified then the bisection can be +aborted early, hopefully before too many mistakes have been made. Then +the user will have to look at what happened and then restart the +bisection using a fixed bisect log. + +There is already a project called BBChop created by Ealdwulf Wuffinga +on Github that does something like that using Bayesian Search Theory +<<9>>: + +_____________ +BBChop is like 'git bisect' (or equivalent), but works when your bug +is intermittent. That is, it works in the presence of false negatives +(when a version happens to work this time even though it contains the +bug). It assumes that there are no false positives (in principle, the +same approach would work, but adding it may be non-trivial). +_____________ + +But BBChop is independent of any VCS and it would be easier for Git +users to have something integrated in Git. + +Conclusion +---------- + +We have seen that regressions are an important problem, and that "git +bisect" has nice features that complement very well practices and +other tools, especially test suites, that are generally used to fight +regressions. But it might be needed to change some work-flows and +(bad) habits to get the most out of it. + +Some improvements to the algorithms inside "git bisect" are possible +and some new features could help in some cases, but overall "git +bisect" works already very well, is used a lot, and is already very +useful. To back up that last claim, let's give the final word to Ingo +Molnar when he was asked by the author how much time does he think +"git bisect" saves him when he uses it: + +_____________ +a _lot_. + +About ten years ago did i do my first 'bisection' of a Linux patch +queue. That was prior the Git (and even prior the BitKeeper) days. I +literally days spent sorting out patches, creating what in essence +were standalone commits that i guessed to be related to that bug. + +It was a tool of absolute last resort. I'd rather spend days looking +at printk output than do a manual 'patch bisection'. + +With Git bisect it's a breeze: in the best case i can get a ~15 step +kernel bisection done in 20-30 minutes, in an automated way. Even with +manual help or when bisecting multiple, overlapping bugs, it's rarely +more than an hour. + +In fact it's invaluable because there are bugs i would never even +_try_ to debug if it wasn't for git bisect. In the past there were bug +patterns that were immediately hopeless for me to debug - at best i +could send the crash/bug signature to lkml and hope that someone else +can think of something. + +And even if a bisection fails today it tells us something valuable +about the bug: that it's non-deterministic - timing or kernel image +layout dependent. + +So git bisect is unconditional goodness - and feel free to quote that +;-) +_____________ + +Acknowledgements +---------------- + +Many thanks to Junio Hamano for his help in reviewing this paper, for +reviewing the patches I sent to the git mailing list, for discussing +some ideas and helping me improve them, for improving "git bisect" a +lot and for his awesome work in maintaining and developing Git. + +Many thanks to Ingo Molnar for giving me very useful information that +appears in this paper, for commenting on this paper, for his +suggestions to improve "git bisect" and for evangelizing "git bisect" +on the linux kernel mailing lists. + +Many thanks to Linus Torvalds for inventing, developing and +evangelizing "git bisect", Git and Linux. + +Many thanks to the many other great people who helped one way or +another when I worked on git, especially to Andreas Ericsson, Johannes +Schindelin, H. Peter Anvin, Daniel Barkalow, Bill Lear, John Hawley, +Shawn O. Pierce, Jeff King, Sam Vilain, Jon Seymour. + +Many thanks to the Linux-Kongress program committee for choosing the +author to given a talk and for publishing this paper. + +References +---------- + +- [[[1]]] http://www.nist.gov/public_affairs/releases/n02-10.htm['Software Errors Cost U.S. Economy $59.5 Billion Annually'. Nist News Release.] +- [[[2]]] http://java.sun.com/docs/codeconv/html/CodeConventions.doc.html#16712['Code Conventions for the Java Programming Language'. Sun Microsystems.] +- [[[3]]] http://en.wikipedia.org/wiki/Software_maintenance['Software maintenance'. Wikipedia.] +- [[[4]]] http://article.gmane.org/gmane.comp.version-control.git/45195/[Junio C Hamano. 'Automated bisect success story'. Gmane.] +- [[[5]]] http://lwn.net/Articles/317154/[Christian Couder. 'Fully automated bisecting with "git bisect run"'. LWN.net.] +- [[[6]]] http://lwn.net/Articles/277872/[Jonathan Corbet. 'Bisection divides users and developers'. LWN.net.] +- [[[7]]] http://article.gmane.org/gmane.linux.scsi/36652/[Ingo Molnar. 'Re: BUG 2.6.23-rc3 can't see sd partitions on Alpha'. Gmane.] +- [[[8]]] http://www.kernel.org/pub/software/scm/git/docs/git-bisect.html[Junio C Hamano and the git-list. 'git-bisect(1) Manual Page'. Linux Kernel Archives.] +- [[[9]]] http://github.com/Ealdwulf/bbchop[Ealdwulf. 'bbchop'. GitHub.] diff --git a/Documentation/git-bisect.txt b/Documentation/git-bisect.txt index d2ffae0c10..c39d957c3a 100644 --- a/Documentation/git-bisect.txt +++ b/Documentation/git-bisect.txt @@ -330,6 +330,11 @@ Documentation ------------- Documentation by Junio C Hamano and the git-list . +SEE ALSO +-------- +link:git-bisect-lk2009.html[Fighting regressions with git bisect], +linkgit:git-blame[1]. + GIT --- Part of the linkgit:git[1] suite diff --git a/Documentation/git-bundle.txt b/Documentation/git-bundle.txt index aee7e4a8c9..c3a066e60c 100644 --- a/Documentation/git-bundle.txt +++ b/Documentation/git-bundle.txt @@ -24,7 +24,7 @@ ssh, rsync, http) cannot be used. This command provides support for 'git-fetch' and 'git-pull' to operate by packaging objects and references in an archive at the originating machine, then importing those into another repository using 'git-fetch' and 'git-pull' -after moving the archive by some means (i.e., by sneakernet). As no +after moving the archive by some means (e.g., by sneakernet). As no direct connection between the repositories exists, the user must specify a basis for the bundle that is held by the destination repository: the bundle assumes that all objects in the basis are already in the diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index 7e7d9fcf50..7ccd742a87 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -11,7 +11,7 @@ SYNOPSIS [verse] 'git clone' [--template=] [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror] - [-o ] [-u ] [--reference ] + [-o ] [-b ] [-u ] [--reference ] [--depth ] [--recursive] [--] [] DESCRIPTION diff --git a/Documentation/git-commit.txt b/Documentation/git-commit.txt index 3ea80c820f..d227cec9ba 100644 --- a/Documentation/git-commit.txt +++ b/Documentation/git-commit.txt @@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] 'git commit' [-a | --interactive] [-s] [-v] [-u] [--amend] [--dry-run] - [(-c | -C) ] [-F | -m ] + [(-c | -C) ] [-F | -m ] [--reset-author] [--allow-empty] [--no-verify] [-e] [--author=] [--cleanup=] [--] [[-i | -o ]...] @@ -69,6 +69,11 @@ OPTIONS Like '-C', but with '-c' the editor is invoked, so that the user can further edit the commit message. +--reset-author:: + When used with -C/-c/--amend options, declare that the + authorship of the resulting commit now belongs of the committer. + This also renews the author timestamp. + -F :: --file=:: Take the commit message from the given file. Use '-' to diff --git a/Documentation/git-cvsserver.txt b/Documentation/git-cvsserver.txt index 785779e221..99a7c14700 100644 --- a/Documentation/git-cvsserver.txt +++ b/Documentation/git-cvsserver.txt @@ -182,10 +182,9 @@ Database Backend ---------------- 'git-cvsserver' uses one database per git head (i.e. CVS module) to -store information about the repository for faster access. The -database doesn't contain any persistent data and can be completely -regenerated from the git repository at any time. The database -needs to be updated (i.e. written to) after every commit. +store information about the repository to maintain consistent +CVS revision numbers. The database needs to be +updated (i.e. written to) after every commit. If the commit is done directly by using `git` (as opposed to using 'git-cvsserver') the update will need to happen on the @@ -204,6 +203,18 @@ write so it might not be enough to grant the users using 'git-cvsserver' write access to the database file without granting them write access to the directory, too. +The database can not be reliably regenerated in a +consistent form after the branch it is tracking has changed. +Example: For merged branches, 'git-cvsserver' only tracks +one branch of development, and after a 'git-merge' an +incrementally updated database may track a different branch +than a database regenerated from scratch, causing inconsistent +CVS revision numbers. `git-cvsserver` has no way of knowing which +branch it would have picked if it had been run incrementally +pre-merge. So if you have to fully or partially (from old +backup) regenerate the database, you should be suspicious +of pre-existing CVS sandboxes. + You can configure the database backend with the following configuration variables: diff --git a/Documentation/git-fetch.txt b/Documentation/git-fetch.txt index f2483d624e..9b9e5686e4 100644 --- a/Documentation/git-fetch.txt +++ b/Documentation/git-fetch.txt @@ -10,11 +10,17 @@ SYNOPSIS -------- 'git fetch' ... +'git fetch' + +'git fetch' --multiple [ | ]... + +'git fetch' --all + DESCRIPTION ----------- -Fetches named heads or tags from another repository, along with -the objects necessary to complete them. +Fetches named heads or tags from one or more other repositories, +along with the objects necessary to complete them. The ref names and their object names of fetched refs are stored in `.git/FETCH_HEAD`. This information is left for a later merge @@ -28,6 +34,10 @@ pointed by remote tags that it does not yet have, then fetch those missing tags. If the other end has tags that point at branches you are not interested in, you will not get them. +'git fetch' can fetch from either a single named repository, or +or from several repositories at once if is given and +there is a remotes. entry in the configuration file. +(See linkgit:git-config[1]). OPTIONS ------- diff --git a/Documentation/git-mailinfo.txt b/Documentation/git-mailinfo.txt index 996c3fcc6c..b81ac98cf0 100644 --- a/Documentation/git-mailinfo.txt +++ b/Documentation/git-mailinfo.txt @@ -8,7 +8,7 @@ git-mailinfo - Extracts patch and authorship from a single e-mail message SYNOPSIS -------- -'git mailinfo' [-k] [-u | --encoding= | -n] [--scissors] +'git mailinfo' [-k|-b] [-u | --encoding= | -n] [--scissors] DESCRIPTION @@ -32,6 +32,11 @@ OPTIONS munging, and is most useful when used to read back 'git-format-patch -k' output. +-b:: + When -k is not in effect, all leading strings bracketed with '[' + and ']' pairs are stripped. This option limits the stripping to + only the pairs whose bracketed string contains the word "PATCH". + -u:: The commit log message, author name and author email are taken from the e-mail, and after minimally decoding MIME diff --git a/Documentation/git-pack-objects.txt b/Documentation/git-pack-objects.txt index 2e4992970e..f54d433d36 100644 --- a/Documentation/git-pack-objects.txt +++ b/Documentation/git-pack-objects.txt @@ -9,8 +9,9 @@ git-pack-objects - Create a packed archive of objects SYNOPSIS -------- [verse] -'git pack-objects' [-q] [--no-reuse-delta] [--delta-base-offset] [--non-empty] - [--local] [--incremental] [--window=N] [--depth=N] [--all-progress] +'git pack-objects' [-q | --progress | --all-progress] [--all-progress-implied] + [--no-reuse-delta] [--delta-base-offset] [--non-empty] + [--local] [--incremental] [--window=N] [--depth=N] [--revs [--unpacked | --all]*] [--stdout | base-name] [--keep-true-parents] < object-list @@ -137,7 +138,7 @@ base-name:: --all-progress:: When --stdout is specified then progress report is - displayed during the object count and deltification phases + displayed during the object count and compression phases but inhibited during the write-out phase. The reason is that in some cases the output stream is directly linked to another command which may wish to display progress @@ -146,6 +147,11 @@ base-name:: report for the write-out phase as well even if --stdout is used. +--all-progress-implied:: + This is used to imply --all-progress whenever progress display + is activated. Unlike --all-progress this flag doesn't actually + force any progress display by itself. + -q:: This flag makes the command not to report its progress on the standard error stream. diff --git a/Documentation/git-replace.txt b/Documentation/git-replace.txt index 8adc1ef55c..65a0da508a 100644 --- a/Documentation/git-replace.txt +++ b/Documentation/git-replace.txt @@ -17,31 +17,35 @@ DESCRIPTION Adds a 'replace' reference in `.git/refs/replace/` The name of the 'replace' reference is the SHA1 of the object that is -replaced. The content of the replace reference is the SHA1 of the +replaced. The content of the 'replace' reference is the SHA1 of the replacement object. -Unless `-f` is given, the replace reference must not yet exist in +Unless `-f` is given, the 'replace' reference must not yet exist in `.git/refs/replace/` directory. -Replace references will be used by default by all git commands except -those doing reachability traversal (prune, pack transfer and fsck). +Replacement references will be used by default by all git commands +except those doing reachability traversal (prune, pack transfer and +fsck). -It is possible to disable use of replacement refs for any command -using the --no-replace-objects option just after "git". +It is possible to disable use of replacement references for any +command using the `--no-replace-objects` option just after 'git'. -For example if commit "foo" has been replaced by commit "bar": +For example if commit 'foo' has been replaced by commit 'bar': ------------------------------------------------ -$ git --no-replace-object cat-file commit foo +$ git --no-replace-objects cat-file commit foo ------------------------------------------------ -show information about commit "foo", while: +shows information about commit 'foo', while: ------------------------------------------------ $ git cat-file commit foo ------------------------------------------------ -show information about commit "bar". +shows information about commit 'bar'. + +The 'GIT_NO_REPLACE_OBJECTS' environment variable can be set to +achieve the same effect as the `--no-replace-objects` option. OPTIONS ------- diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt index c85d7f4385..8c482f40b9 100644 --- a/Documentation/git-send-email.txt +++ b/Documentation/git-send-email.txt @@ -108,9 +108,10 @@ Sending --envelope-sender=
:: Specify the envelope sender used to send the emails. This is useful if your default address is not the address that is - subscribed to a list. If you use the sendmail binary, you must have - suitable privileges for the -f parameter. Default is the value of - the 'sendemail.envelopesender' configuration variable; if that is + subscribed to a list. In order to use the 'From' address, set the + value to "auto". If you use the sendmail binary, you must have + suitable privileges for the -f parameter. Default is the value of the + 'sendemail.envelopesender' configuration variable; if that is unspecified, choosing the envelope sender is left to your MTA. --smtp-encryption=:: diff --git a/Documentation/gitcore-tutorial.txt b/Documentation/gitcore-tutorial.txt index b7380b069a..f762dca440 100644 --- a/Documentation/gitcore-tutorial.txt +++ b/Documentation/gitcore-tutorial.txt @@ -602,7 +602,7 @@ $ git tag -s ---------------- which will sign the current `HEAD` (but you can also give it another -argument that specifies the thing to tag, i.e., you could have tagged the +argument that specifies the thing to tag, e.g., you could have tagged the current `mybranch` point by using `git tag mybranch`). You normally only do signed tags for major releases or things @@ -1186,9 +1186,9 @@ $ git show-branch * [master] Some fun. ! [mybranch] Some work. -- - + [mybranch] Some work. * [master] Some fun. -*+ [mybranch^] Initial commit + + [mybranch] Some work. +*+ [master^] Initial commit ------------ Now we are ready to experiment with the merge by hand. diff --git a/Documentation/gitworkflows.txt b/Documentation/gitworkflows.txt index 91c0eea890..065441df64 100644 --- a/Documentation/gitworkflows.txt +++ b/Documentation/gitworkflows.txt @@ -229,7 +229,7 @@ To verify that 'master' is indeed a superset of 'maint', use git log: .Verify 'master' is a superset of 'maint' [caption="Recipe: "] ===================================== -git log master..maint +`git log master..maint` ===================================== This command should not list any commits. Otherwise, check out diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt index 7ff6a6ce22..53a9168ba7 100644 --- a/Documentation/pretty-formats.txt +++ b/Documentation/pretty-formats.txt @@ -136,9 +136,7 @@ The placeholders are: - '%n': newline - '%x00': print a byte from a hex code - '%w([[,[,]]])': switch line wrapping, like the -w option of - linkgit:git-shortlog[1]. NOTE: Color placeholders (`%C*`) are not - recognized as having no width, so they should not be put into wrapped - sections. + linkgit:git-shortlog[1]. NOTE: Some placeholders may depend on other options given to the revision traversal engine. For example, the `%g*` reflog options will @@ -146,6 +144,14 @@ insert an empty string unless we are traversing reflog entries (e.g., by `git log -g`). The `%d` placeholder will use the "short" decoration format if `--decorate` was not already provided on the command line. +If you add a `{plus}` (plus sign) after '%' of a placeholder, a line-feed +is inserted immediately before the expansion if and only if the +placeholder expands to a non-empty string. + +If you add a `-` (minus sign) after '%' of a placeholder, line-feeds that +immediately precede the expansion are deleted if and only if the +placeholder expands to an empty string. + * 'tformat:' + The 'tformat:' format works exactly like 'format:', except that it diff --git a/Documentation/pull-fetch-param.txt b/Documentation/pull-fetch-param.txt index 44d936341f..0551ebdfaf 100644 --- a/Documentation/pull-fetch-param.txt +++ b/Documentation/pull-fetch-param.txt @@ -4,6 +4,13 @@ (see the section <> below) or the name of a remote (see the section <> below). +ifndef::git-pull[] +:: + A name referring to a list of repositories as the value + of remotes. in the configuration file. + (See linkgit:git-config[1]). +endif::git-pull[] + :: The format of a parameter is an optional plus `{plus}`, followed by the source ref , followed diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt index bf66116d61..1f57aed337 100644 --- a/Documentation/rev-list-options.txt +++ b/Documentation/rev-list-options.txt @@ -243,12 +243,23 @@ endif::git-rev-list[] Pretend as if all the refs in `$GIT_DIR/refs/remotes` are listed on the command line as ''. -ifdef::git-rev-list[] +ifndef::git-rev-list[] +--bisect:: + + Pretend as if the bad bisection ref `$GIT_DIR/refs/bisect/bad` + was listed and as if it was followed by `--not` and the good + bisection refs `$GIT_DIR/refs/bisect/good-*` on the command + line. +endif::git-rev-list[] + --stdin:: In addition to the '' listed on the command - line, read them from the standard input. + line, read them from the standard input. If a '--' separator is + seen, stop reading commits and start reading paths to limit the + result. +ifdef::git-rev-list[] --quiet:: Don't print anything to standard output. This form @@ -536,7 +547,11 @@ Bisection Helpers --bisect:: Limit output to the one commit object which is roughly halfway between -the included and excluded commits. Thus, if +included and excluded commits. Note that the bad bisection ref +`$GIT_DIR/refs/bisect/bad` is added to the included commits (if it +exists) and the good bisection refs `$GIT_DIR/refs/bisect/good-*` are +added to the excluded commits (if they exist). Thus, supposing there +are no refs in `$GIT_DIR/refs/bisect/`, if ----------------------------------------------------------------------- $ git rev-list --bisect foo ^bar ^baz @@ -556,22 +571,24 @@ one. --bisect-vars:: -This calculates the same as `--bisect`, but outputs text ready -to be eval'ed by the shell. These lines will assign the name of -the midpoint revision to the variable `bisect_rev`, and the -expected number of commits to be tested after `bisect_rev` is -tested to `bisect_nr`, the expected number of commits to be -tested if `bisect_rev` turns out to be good to `bisect_good`, -the expected number of commits to be tested if `bisect_rev` -turns out to be bad to `bisect_bad`, and the number of commits -we are bisecting right now to `bisect_all`. +This calculates the same as `--bisect`, except that refs in +`$GIT_DIR/refs/bisect/` are not used, and except that this outputs +text ready to be eval'ed by the shell. These lines will assign the +name of the midpoint revision to the variable `bisect_rev`, and the +expected number of commits to be tested after `bisect_rev` is tested +to `bisect_nr`, the expected number of commits to be tested if +`bisect_rev` turns out to be good to `bisect_good`, the expected +number of commits to be tested if `bisect_rev` turns out to be bad to +`bisect_bad`, and the number of commits we are bisecting right now to +`bisect_all`. --bisect-all:: This outputs all the commit objects between the included and excluded commits, ordered by their distance to the included and excluded -commits. The farthest from them is displayed first. (This is the only -one displayed by `--bisect`.) +commits. Refs in `$GIT_DIR/refs/bisect/` are not used. The farthest +from them is displayed first. (This is the only one displayed by +`--bisect`.) + This is useful because it makes it easy to choose a good commit to test when you want to avoid to test some of them for some reason (they diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt index 27d97b6d1e..b169836684 100644 --- a/Documentation/user-manual.txt +++ b/Documentation/user-manual.txt @@ -1183,7 +1183,23 @@ $ git merge branchname ------------------------------------------------- merges the development in the branch "branchname" into the current -branch. If there are conflicts--for example, if the same file is +branch. + +A merge is made by combining the changes made in "branchname" and the +changes made up to the latest commit in your current branch since +their histories forked. The work tree is overwritten by the result of +the merge when this combining is done cleanly, or overwritten by a +half-merged results when this combining results in conflicts. +Therefore, if you have uncommitted changes touching the same files as +the ones impacted by the merge, Git will refuse to proceed. Most of +the time, you will want to commit your changes before you can merge, +and if you don't, then linkgit:git-stash[1] can take these changes +away while you're doing the merge, and reapply them afterwards. + +If the changes are independant enough, Git will automatically complete +the merge and commit the result (or reuse an existing commit in case +of <>, see below). On the other hand, +if there are conflicts--for example, if the same file is modified in two different ways in the remote branch and the local branch--then you are warned; the output may look something like this: @@ -1679,7 +1695,7 @@ Sharing development with others Getting updates with git pull ----------------------------- -After you clone a repository and make a few changes of your own, you +After you clone a repository and commit a few changes of your own, you may wish to check the original repository for updates and merge them into your own work. diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 710d361233..10a38ba67a 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v1.6.5.GIT +DEF_VER=v1.6.6-rc1.GIT LF=' ' diff --git a/Makefile b/Makefile index 25a977153c..4a1e5bcc4d 100644 --- a/Makefile +++ b/Makefile @@ -205,6 +205,9 @@ all:: # # Define NO_REGEX if you have no or inferior regex support in your C library. # +# Define JSMIN to point to JavaScript minifier that functions as +# a filter to have gitweb.js minified. +# # Define DEFAULT_PAGER to a sensible pager command (defaults to "less") if # you want to use something different. The value will be interpreted by the # shell at runtime when it is used. @@ -274,6 +277,9 @@ lib = lib # DESTDIR= pathsep = : +# JavaScript minifier invocation that can function as filter +JSMIN = + # default configuration for gitweb GITWEB_CONFIG = gitweb_config.perl GITWEB_CONFIG_SYSTEM = /etc/gitweb.conf @@ -289,6 +295,11 @@ GITWEB_HOMETEXT = indextext.html GITWEB_CSS = gitweb.css GITWEB_LOGO = git-logo.png GITWEB_FAVICON = git-favicon.png +ifdef JSMIN +GITWEB_JS = gitweb.min.js +else +GITWEB_JS = gitweb.js +endif GITWEB_SITE_HEADER = GITWEB_SITE_FOOTER = @@ -805,6 +816,7 @@ ifeq ($(uname_O),Cygwin) NO_FAST_WORKING_DIRECTORY = UnfortunatelyYes NO_TRUSTABLE_FILEMODE = UnfortunatelyYes OLD_ICONV = UnfortunatelyYes + NO_ST_BLOCKS_IN_STRUCT_STAT = YesPlease # There are conflicting reports about this. # On some boxes NO_MMAP is needed, and not so elsewhere. # Try commenting this out if you suspect MMAP is more efficient @@ -982,7 +994,6 @@ ifneq (,$(findstring MINGW,$(uname_S))) NEEDS_CRYPTO_WITH_SSL = YesPlease NO_LIBGEN_H = YesPlease NO_SYMLINK_HEAD = YesPlease - NO_IPV6 = YesPlease NO_SETENV = YesPlease NO_UNSETENV = YesPlease NO_STRCASESTR = YesPlease @@ -1498,8 +1509,13 @@ $(patsubst %.perl,%,$(SCRIPT_PERL)): % : %.perl chmod +x $@+ && \ mv $@+ $@ +ifdef JSMIN +OTHER_PROGRAMS += gitweb/gitweb.cgi gitweb/gitweb.min.js +gitweb/gitweb.cgi: gitweb/gitweb.perl gitweb/gitweb.min.js +else OTHER_PROGRAMS += gitweb/gitweb.cgi gitweb/gitweb.cgi: gitweb/gitweb.perl +endif $(QUIET_GEN)$(RM) $@ $@+ && \ sed -e '1s|#!.*perl|#!$(PERL_PATH_SQ)|' \ -e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \ @@ -1518,13 +1534,14 @@ gitweb/gitweb.cgi: gitweb/gitweb.perl -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \ -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \ -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \ + -e 's|++GITWEB_JS++|$(GITWEB_JS)|g' \ -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \ -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \ $< >$@+ && \ chmod +x $@+ && \ mv $@+ $@ -git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css +git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css gitweb/gitweb.js $(QUIET_GEN)$(RM) $@ $@+ && \ sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ @@ -1533,6 +1550,8 @@ git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css -e '/@@GITWEB_CGI@@/d' \ -e '/@@GITWEB_CSS@@/r gitweb/gitweb.css' \ -e '/@@GITWEB_CSS@@/d' \ + -e '/@@GITWEB_JS@@/r gitweb/gitweb.js' \ + -e '/@@GITWEB_JS@@/d' \ -e 's|@@PERL@@|$(PERL_PATH_SQ)|g' \ $@.sh > $@+ && \ chmod +x $@+ && \ @@ -1547,6 +1566,11 @@ $(patsubst %.perl,%,$(SCRIPT_PERL)) git-instaweb: % : unimplemented.sh mv $@+ $@ endif # NO_PERL +ifdef JSMIN +gitweb/gitweb.min.js: gitweb/gitweb.js + $(QUIET_GEN)$(JSMIN) <$< >$@ +endif # JSMIN + configure: configure.ac $(QUIET_GEN)$(RM) $@ $<+ && \ sed -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ @@ -1874,7 +1898,7 @@ distclean: clean $(RM) configure clean: - $(RM) *.o block-sha1/*.o arm/*.o ppc/*.o compat/*.o compat/*/*.o xdiff/*.o \ + $(RM) *.o block-sha1/*.o ppc/*.o compat/*.o compat/*/*.o xdiff/*.o \ $(LIB_FILE) $(XDIFF_LIB) $(RM) $(ALL_PROGRAMS) $(BUILT_INS) git$X $(RM) $(TEST_PROGRAMS) diff --git a/advice.c b/advice.c index ae4b1e81df..cb666acc3c 100644 --- a/advice.c +++ b/advice.c @@ -2,6 +2,7 @@ int advice_push_nonfastforward = 1; int advice_status_hints = 1; +int advice_commit_before_merge = 1; static struct { const char *name; @@ -9,6 +10,7 @@ static struct { } advice_config[] = { { "pushnonfastforward", &advice_push_nonfastforward }, { "statushints", &advice_status_hints }, + { "commitbeforemerge", &advice_commit_before_merge }, }; int git_default_advice_config(const char *var, const char *value) diff --git a/advice.h b/advice.h index e9df8e026c..3de5000d8c 100644 --- a/advice.h +++ b/advice.h @@ -3,6 +3,7 @@ extern int advice_push_nonfastforward; extern int advice_status_hints; +extern int advice_commit_before_merge; int git_default_advice_config(const char *var, const char *value); diff --git a/builtin-apply.c b/builtin-apply.c index f667368d16..36e2f9dda5 100644 --- a/builtin-apply.c +++ b/builtin-apply.c @@ -823,12 +823,13 @@ static int gitdiff_unrecognized(const char *line, struct patch *patch) static const char *stop_at_slash(const char *line, int llen) { + int nslash = p_value; int i; for (i = 0; i < llen; i++) { int ch = line[i]; - if (ch == '/') - return line + i; + if (ch == '/' && --nslash <= 0) + return &line[i]; } return NULL; } diff --git a/builtin-blame.c b/builtin-blame.c index dd16b22297..d4e25a595d 100644 --- a/builtin-blame.c +++ b/builtin-blame.c @@ -2358,6 +2358,7 @@ parse_done: die_errno("cannot stat path '%s'", path); } + revs.disable_stdin = 1; setup_revisions(argc, argv, &revs, NULL); memset(&sb, 0, sizeof(sb)); diff --git a/builtin-commit.c b/builtin-commit.c index 09d28405ec..e93a647c59 100644 --- a/builtin-commit.c +++ b/builtin-commit.c @@ -51,7 +51,7 @@ static const char *template_file; static char *edit_message, *use_message; static char *author_name, *author_email, *author_date; static int all, edit_flag, also, interactive, only, amend, signoff; -static int quiet, verbose, no_verify, allow_empty, dry_run; +static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship; static char *untracked_files_arg; /* * The default commit message cleanup mode will remove the lines @@ -91,8 +91,9 @@ static struct option builtin_commit_options[] = { OPT_FILENAME('F', "file", &logfile, "read log from file"), OPT_STRING(0, "author", &force_author, "AUTHOR", "override author for commit"), OPT_CALLBACK('m', "message", &message, "MESSAGE", "specify commit message", opt_parse_m), - OPT_STRING('c', "reedit-message", &edit_message, "COMMIT", "reuse and edit message from specified commit "), + OPT_STRING('c', "reedit-message", &edit_message, "COMMIT", "reuse and edit message from specified commit"), OPT_STRING('C', "reuse-message", &use_message, "COMMIT", "reuse message from specified commit"), + OPT_BOOLEAN(0, "reset-author", &renew_authorship, "the commit is authored by me now (used with -C-c/--amend)"), OPT_BOOLEAN('s', "signoff", &signoff, "add Signed-off-by:"), OPT_FILENAME('t', "template", &template_file, "use specified template file"), OPT_BOOLEAN('e', "edit", &edit_flag, "force edit of commit"), @@ -381,7 +382,7 @@ static void determine_author_info(void) email = getenv("GIT_AUTHOR_EMAIL"); date = getenv("GIT_AUTHOR_DATE"); - if (use_message) { + if (use_message && !renew_authorship) { const char *a, *lb, *rb, *eol; a = strstr(use_message_buffer, "\nauthor "); @@ -747,6 +748,9 @@ static int parse_and_validate_options(int argc, const char *argv[], if (force_author && !strchr(force_author, '>')) force_author = find_author_by_nickname(force_author); + if (force_author && renew_authorship) + die("Using both --reset-author and --author does not make sense"); + if (logfile || message.len || use_message) use_editor = 0; if (edit_flag) @@ -780,6 +784,8 @@ static int parse_and_validate_options(int argc, const char *argv[], use_message = edit_message; if (amend && !use_message) use_message = "HEAD"; + if (!use_message && renew_authorship) + die("--reset-author can be used only with -C, -c or --amend."); if (use_message) { unsigned char sha1[20]; static char utf8[] = "UTF-8"; diff --git a/builtin-diff-tree.c b/builtin-diff-tree.c index 79cedb72c4..2380c21951 100644 --- a/builtin-diff-tree.c +++ b/builtin-diff-tree.c @@ -104,6 +104,7 @@ int cmd_diff_tree(int argc, const char **argv, const char *prefix) git_config(git_diff_basic_config, NULL); /* no "diff" UI options */ opt->abbrev = 0; opt->diff = 1; + opt->disable_stdin = 1; argc = setup_revisions(argc, argv, opt, NULL); while (--argc > 0) { diff --git a/builtin-fetch.c b/builtin-fetch.c index cd0bcf7140..5b7db616dc 100644 --- a/builtin-fetch.c +++ b/builtin-fetch.c @@ -14,6 +14,9 @@ static const char * const builtin_fetch_usage[] = { "git fetch [options] [ ...]", + "git fetch [options] ", + "git fetch --multiple [options] [ | ]...", + "git fetch --all [options]", NULL }; @@ -23,7 +26,7 @@ enum { TAGS_SET = 2 }; -static int append, force, keep, update_head_ok, verbosity; +static int all, append, dry_run, force, keep, multiple, prune, update_head_ok, verbosity; static int tags = TAGS_DEFAULT; static const char *depth; static const char *upload_pack; @@ -32,16 +35,24 @@ static struct transport *transport; static struct option builtin_fetch_options[] = { OPT__VERBOSITY(&verbosity), + OPT_BOOLEAN(0, "all", &all, + "fetch from all remotes"), OPT_BOOLEAN('a', "append", &append, "append to .git/FETCH_HEAD instead of overwriting"), OPT_STRING(0, "upload-pack", &upload_pack, "PATH", "path to upload pack on remote end"), OPT_BOOLEAN('f', "force", &force, "force overwrite of local branch"), + OPT_BOOLEAN('m', "multiple", &multiple, + "fetch from multiple remotes"), OPT_SET_INT('t', "tags", &tags, "fetch all tags and associated objects", TAGS_SET), OPT_SET_INT('n', NULL, &tags, "do not fetch all tags (--no-tags)", TAGS_UNSET), + OPT_BOOLEAN('p', "prune", &prune, + "prune tracking branches no longer on remote"), + OPT_BOOLEAN(0, "dry-run", &dry_run, + "dry run"), OPT_BOOLEAN('k', "keep", &keep, "keep downloaded pack"), OPT_BOOLEAN('u', "update-head-ok", &update_head_ok, "allow updating of HEAD ref"), @@ -178,6 +189,8 @@ static int s_update_ref(const char *action, char *rla = getenv("GIT_REFLOG_ACTION"); static struct ref_lock *lock; + if (dry_run) + return 0; if (!rla) rla = default_rla.buf; snprintf(msg, sizeof(msg), "%s: %s", rla, action); @@ -303,7 +316,7 @@ static int store_updated_refs(const char *raw_url, const char *remote_name, char note[1024]; const char *what, *kind; struct ref *rm; - char *url, *filename = git_path("FETCH_HEAD"); + char *url, *filename = dry_run ? "/dev/null" : git_path("FETCH_HEAD"); fp = fopen(filename, "a"); if (!fp) @@ -485,6 +498,28 @@ static int fetch_refs(struct transport *transport, struct ref *ref_map) return ret; } +static int prune_refs(struct transport *transport, struct ref *ref_map) +{ + int result = 0; + struct ref *ref, *stale_refs = get_stale_heads(transport->remote, ref_map); + const char *dangling_msg = dry_run + ? " (%s will become dangling)\n" + : " (%s has become dangling)\n"; + + for (ref = stale_refs; ref; ref = ref->next) { + if (!dry_run) + result |= delete_ref(ref->name, NULL, 0); + if (verbosity >= 0) { + fprintf(stderr, " x %-*s %-*s -> %s\n", + SUMMARY_WIDTH, "[deleted]", + REFCOL_WIDTH, "(none)", prettify_refname(ref->name)); + warn_dangling_symref(stderr, dangling_msg, ref->name); + } + } + free_refs(stale_refs); + return result; +} + static int add_existing(const char *refname, const unsigned char *sha1, int flag, void *cbdata) { @@ -633,7 +668,7 @@ static int do_fetch(struct transport *transport, die("Don't know how to fetch from %s", transport->url); /* if not appending, truncate FETCH_HEAD */ - if (!append) { + if (!append && !dry_run) { char *filename = git_path("FETCH_HEAD"); FILE *fp = fopen(filename, "w"); if (!fp) @@ -661,6 +696,8 @@ static int do_fetch(struct transport *transport, free_refs(ref_map); return 1; } + if (prune) + prune_refs(transport, ref_map); free_refs(ref_map); /* if neither --no-tags nor --tags was specified, do automated tag @@ -691,27 +728,94 @@ static void set_option(const char *name, const char *value) name, transport->url); } -int cmd_fetch(int argc, const char **argv, const char *prefix) +static int get_one_remote_for_fetch(struct remote *remote, void *priv) +{ + struct string_list *list = priv; + if (!remote->skip_default_update) + string_list_append(remote->name, list); + return 0; +} + +struct remote_group_data { + const char *name; + struct string_list *list; +}; + +static int get_remote_group(const char *key, const char *value, void *priv) +{ + struct remote_group_data *g = priv; + + if (!prefixcmp(key, "remotes.") && + !strcmp(key + 8, g->name)) { + /* split list by white space */ + int space = strcspn(value, " \t\n"); + while (*value) { + if (space > 1) { + string_list_append(xstrndup(value, space), + g->list); + } + value += space + (value[space] != '\0'); + space = strcspn(value, " \t\n"); + } + } + + return 0; +} + +static int add_remote_or_group(const char *name, struct string_list *list) +{ + int prev_nr = list->nr; + struct remote_group_data g = { name, list }; + + git_config(get_remote_group, &g); + if (list->nr == prev_nr) { + struct remote *remote; + if (!remote_is_configured(name)) + return 0; + remote = remote_get(name); + string_list_append(remote->name, list); + } + return 1; +} + +static int fetch_multiple(struct string_list *list) +{ + int i, result = 0; + const char *argv[] = { "fetch", NULL, NULL, NULL, NULL, NULL, NULL }; + int argc = 1; + + if (dry_run) + argv[argc++] = "--dry-run"; + if (prune) + argv[argc++] = "--prune"; + if (verbosity >= 2) + argv[argc++] = "-v"; + if (verbosity >= 1) + argv[argc++] = "-v"; + else if (verbosity < 0) + argv[argc++] = "-q"; + + for (i = 0; i < list->nr; i++) { + const char *name = list->items[i].string; + argv[argc] = name; + if (verbosity >= 0) + printf("Fetching %s\n", name); + if (run_command_v_opt(argv, RUN_GIT_CMD)) { + error("Could not fetch %s", name); + result = 1; + } + } + + return result; +} + +static int fetch_one(struct remote *remote, int argc, const char **argv) { - struct remote *remote; int i; static const char **refs = NULL; int ref_nr = 0; int exit_code; - /* Record the command line for the reflog */ - strbuf_addstr(&default_rla, "fetch"); - for (i = 1; i < argc; i++) - strbuf_addf(&default_rla, " %s", argv[i]); - - argc = parse_options(argc, argv, prefix, - builtin_fetch_options, builtin_fetch_usage, 0); - - if (argc == 0) - remote = remote_get(NULL); - else - remote = remote_get(argv[0]); - if (!remote) die("Where do you want to fetch from today?"); @@ -727,10 +831,10 @@ int cmd_fetch(int argc, const char **argv, const char *prefix) if (depth) set_option(TRANS_OPT_DEPTH, depth); - if (argc > 1) { + if (argc > 0) { int j = 0; refs = xcalloc(argc + 1, sizeof(const char *)); - for (i = 1; i < argc; i++) { + for (i = 0; i < argc; i++) { if (!strcmp(argv[i], "tag")) { char *ref; i++; @@ -757,3 +861,57 @@ int cmd_fetch(int argc, const char **argv, const char *prefix) transport = NULL; return exit_code; } + +int cmd_fetch(int argc, const char **argv, const char *prefix) +{ + int i; + struct string_list list = { NULL, 0, 0, 0 }; + struct remote *remote; + int result = 0; + + /* Record the command line for the reflog */ + strbuf_addstr(&default_rla, "fetch"); + for (i = 1; i < argc; i++) + strbuf_addf(&default_rla, " %s", argv[i]); + + argc = parse_options(argc, argv, prefix, + builtin_fetch_options, builtin_fetch_usage, 0); + + if (all) { + if (argc == 1) + die("fetch --all does not take a repository argument"); + else if (argc > 1) + die("fetch --all does not make sense with refspecs"); + (void) for_each_remote(get_one_remote_for_fetch, &list); + result = fetch_multiple(&list); + } else if (argc == 0) { + /* No arguments -- use default remote */ + remote = remote_get(NULL); + result = fetch_one(remote, argc, argv); + } else if (multiple) { + /* All arguments are assumed to be remotes or groups */ + for (i = 0; i < argc; i++) + if (!add_remote_or_group(argv[i], &list)) + die("No such remote or remote group: %s", argv[i]); + result = fetch_multiple(&list); + } else { + /* Single remote or group */ + (void) add_remote_or_group(argv[0], &list); + if (list.nr > 1) { + /* More than one remote */ + if (argc > 1) + die("Fetching a group and specifying refspecs does not make sense"); + result = fetch_multiple(&list); + } else { + /* Zero or one remotes */ + remote = remote_get(argv[0]); + result = fetch_one(remote, argc-1, argv+1); + } + } + + /* All names were strdup()ed or strndup()ed */ + list.strdup_strings = 1; + string_list_clear(&list, 0); + + return result; +} diff --git a/builtin-help.c b/builtin-help.c index ca08519d9d..09ad4b04f9 100644 --- a/builtin-help.c +++ b/builtin-help.c @@ -427,9 +427,6 @@ int cmd_help(int argc, const char **argv, const char *prefix) return 0; } - setup_git_directory_gently(&nongit); - git_config(git_help_config, NULL); - if (!argv[0]) { printf("usage: %s\n\n", git_usage_string); list_common_cmds_help(); @@ -437,6 +434,9 @@ int cmd_help(int argc, const char **argv, const char *prefix) return 0; } + setup_git_directory_gently(&nongit); + git_config(git_help_config, NULL); + alias = alias_lookup(argv[0]); if (alias && !is_git_command(argv[0])) { printf("`git %s' is aliased to `%s'\n", argv[0], alias); diff --git a/builtin-log.c b/builtin-log.c index 33fa6ea6c8..1766349550 100644 --- a/builtin-log.c +++ b/builtin-log.c @@ -976,7 +976,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix) */ argc = parse_options(argc, argv, prefix, builtin_format_patch_options, builtin_format_patch_usage, - PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN); + PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN | + PARSE_OPT_KEEP_DASHDASH); if (do_signoff) { const char *committer; diff --git a/builtin-mailinfo.c b/builtin-mailinfo.c index 3c4f0753fe..a50ac2256c 100644 --- a/builtin-mailinfo.c +++ b/builtin-mailinfo.c @@ -10,6 +10,7 @@ static FILE *cmitmsg, *patchfile, *fin, *fout; static int keep_subject; +static int keep_non_patch_brackets_in_subject; static const char *metainfo_charset; static struct strbuf line = STRBUF_INIT; static struct strbuf name = STRBUF_INIT; @@ -221,35 +222,41 @@ static int is_multipart_boundary(const struct strbuf *line) static void cleanup_subject(struct strbuf *subject) { - char *pos; - size_t remove; - while (subject->len) { - switch (*subject->buf) { + size_t at = 0; + + while (at < subject->len) { + char *pos; + size_t remove; + + switch (subject->buf[at]) { case 'r': case 'R': - if (subject->len <= 3) + if (subject->len <= at + 3) break; - if (!memcmp(subject->buf + 1, "e:", 2)) { - strbuf_remove(subject, 0, 3); + if (!memcmp(subject->buf + at + 1, "e:", 2)) { + strbuf_remove(subject, at, 3); continue; } + at++; break; case ' ': case '\t': case ':': - strbuf_remove(subject, 0, 1); + strbuf_remove(subject, at, 1); continue; case '[': - if ((pos = strchr(subject->buf, ']'))) { - remove = pos - subject->buf; - if (remove <= (subject->len - remove) * 2) { - strbuf_remove(subject, 0, remove + 1); - continue; - } - } else - strbuf_remove(subject, 0, 1); - break; + pos = strchr(subject->buf + at, ']'); + if (!pos) + break; + remove = pos - subject->buf + at + 1; + if (!keep_non_patch_brackets_in_subject || + (7 <= remove && + memmem(subject->buf + at, remove, "PATCH", 5))) + strbuf_remove(subject, at, remove); + else + at += remove; + continue; } - strbuf_trim(subject); - return; + break; } + strbuf_trim(subject); } static void cleanup_space(struct strbuf *sb) @@ -1014,7 +1021,7 @@ static int git_mailinfo_config(const char *var, const char *value, void *unused) } static const char mailinfo_usage[] = - "git mailinfo [-k] [-u | --encoding= | -n] [--scissors | --no-scissors] msg patch < mail >info"; + "git mailinfo [-k|-b] [-u | --encoding= | -n] [--scissors | --no-scissors] msg patch < mail >info"; int cmd_mailinfo(int argc, const char **argv, const char *prefix) { @@ -1031,6 +1038,8 @@ int cmd_mailinfo(int argc, const char **argv, const char *prefix) while (1 < argc && argv[1][0] == '-') { if (!strcmp(argv[1], "-k")) keep_subject = 1; + else if (!strcmp(argv[1], "-b")) + keep_non_patch_brackets_in_subject = 1; else if (!strcmp(argv[1], "-u")) metainfo_charset = def_charset; else if (!strcmp(argv[1], "-n")) diff --git a/builtin-merge.c b/builtin-merge.c index 57eedd447d..56a1bb651f 100644 --- a/builtin-merge.c +++ b/builtin-merge.c @@ -71,7 +71,7 @@ static int option_parse_message(const struct option *opt, if (unset) strbuf_setlen(buf, 0); else if (arg) { - strbuf_addf(buf, "%s\n\n", arg); + strbuf_addf(buf, "%s%s", buf->len ? "\n\n" : "", arg); have_message = 1; } else return error("switch `m' requires a value"); @@ -107,8 +107,8 @@ static struct strategy *get_strategy(const char *name) found = 1; if (!found) add_cmdname(¬_strategies, ent->name, ent->len); - exclude_cmds(&main_cmds, ¬_strategies); } + exclude_cmds(&main_cmds, ¬_strategies); } if (!is_in_cmdlist(&main_cmds, name) && !is_in_cmdlist(&other_cmds, name)) { fprintf(stderr, "Could not find merge strategy '%s'.\n", name); @@ -656,6 +656,7 @@ static int checkout_fast_forward(unsigned char *head, unsigned char *remote) opts.verbose_update = 1; opts.merge = 1; opts.fn = twoway_merge; + opts.msgs = get_porcelain_error_msgs(); trees[nr_trees] = parse_tree_indirect(head); if (!trees[nr_trees++]) @@ -795,10 +796,15 @@ static int suggest_conflicts(void) return 1; } +static const char deprecation_warning[] = + "'git merge HEAD ' is deprecated. Please update\n" + "your script to use 'git merge -m ' instead.\n" + "In future versions of git, this syntax will be removed."; + static struct commit *is_old_style_invocation(int argc, const char **argv) { struct commit *second_token = NULL; - if (argc > 1) { + if (argc > 2) { unsigned char second_sha1[20]; if (get_sha1(argv[1], second_sha1)) @@ -808,6 +814,7 @@ static struct commit *is_old_style_invocation(int argc, const char **argv) die("'%s' is not a commit", argv[1]); if (hashcmp(second_token->object.sha1, head)) return NULL; + warning(deprecation_warning); } return second_token; } @@ -936,11 +943,13 @@ int cmd_merge(int argc, const char **argv, const char *prefix) * codepath so we discard the error in this * loop. */ - for (i = 0; i < argc; i++) - merge_name(argv[i], &msg); - fmt_merge_msg(option_log, &msg, &merge_msg); - if (merge_msg.len) - strbuf_setlen(&merge_msg, merge_msg.len-1); + if (!have_message) { + for (i = 0; i < argc; i++) + merge_name(argv[i], &msg); + fmt_merge_msg(option_log, &msg, &merge_msg); + if (merge_msg.len) + strbuf_setlen(&merge_msg, merge_msg.len-1); + } } if (head_invalid || !argc) diff --git a/builtin-pack-objects.c b/builtin-pack-objects.c index 4c91e944c2..4429d53a1e 100644 --- a/builtin-pack-objects.c +++ b/builtin-pack-objects.c @@ -24,6 +24,7 @@ static const char pack_usage[] = "git pack-objects [{ -q | --progress | --all-progress }]\n" + " [--all-progress-implied]\n" " [--max-pack-size=N] [--local] [--incremental]\n" " [--window=N] [--window-memory=N] [--depth=N]\n" " [--no-reuse-delta] [--no-reuse-object] [--delta-base-offset]\n" @@ -2124,6 +2125,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) { int use_internal_rev_list = 0; int thin = 0; + int all_progress_implied = 0; uint32_t i; const char **rp_av; int rp_ac_alloc = 64; @@ -2223,6 +2225,10 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) progress = 2; continue; } + if (!strcmp("--all-progress-implied", arg)) { + all_progress_implied = 1; + continue; + } if (!strcmp("-q", arg)) { progress = 0; continue; @@ -2326,6 +2332,9 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) if (keep_unreachable && unpack_unreachable) die("--keep-unreachable and --unpack-unreachable are incompatible."); + if (progress && all_progress_implied) + progress = 2; + prepare_packed_git(); if (progress) diff --git a/builtin-prune-packed.c b/builtin-prune-packed.c index be99eb0ac4..f9463deec2 100644 --- a/builtin-prune-packed.c +++ b/builtin-prune-packed.c @@ -71,7 +71,7 @@ void prune_packed_objects(int opts) int cmd_prune_packed(int argc, const char **argv, const char *prefix) { - int opts = VERBOSE; + int opts = isatty(2) ? VERBOSE : 0; const struct option prune_packed_options[] = { OPT_BIT('n', "dry-run", &opts, "dry run", DRY_RUN), OPT_NEGBIT('q', "quiet", &opts, "be quiet", VERBOSE), diff --git a/builtin-remote.c b/builtin-remote.c index b08608ef96..a5019397ff 100644 --- a/builtin-remote.c +++ b/builtin-remote.c @@ -261,32 +261,10 @@ struct ref_states { int queried; }; -static int handle_one_branch(const char *refname, - const unsigned char *sha1, int flags, void *cb_data) -{ - struct ref_states *states = cb_data; - struct refspec refspec; - - memset(&refspec, 0, sizeof(refspec)); - refspec.dst = (char *)refname; - if (!remote_find_tracking(states->remote, &refspec)) { - struct string_list_item *item; - const char *name = abbrev_branch(refspec.src); - /* symbolic refs pointing nowhere were handled already */ - if ((flags & REF_ISSYMREF) || - string_list_has_string(&states->tracked, name) || - string_list_has_string(&states->new, name)) - return 0; - item = string_list_append(name, &states->stale); - item->util = xstrdup(refname); - } - return 0; -} - static int get_ref_states(const struct ref *remote_refs, struct ref_states *states) { struct ref *fetch_map = NULL, **tail = &fetch_map; - struct ref *ref; + struct ref *ref, *stale_refs; int i; for (i = 0; i < states->remote->fetch_refspec_nr; i++) @@ -294,7 +272,9 @@ static int get_ref_states(const struct ref *remote_refs, struct ref_states *stat die("Could not get fetch map for refspec %s", states->remote->fetch_refspec[i]); - states->new.strdup_strings = states->tracked.strdup_strings = 1; + states->new.strdup_strings = 1; + states->tracked.strdup_strings = 1; + states->stale.strdup_strings = 1; for (ref = fetch_map; ref; ref = ref->next) { unsigned char sha1[20]; if (!ref->peer_ref || read_ref(ref->peer_ref->name, sha1)) @@ -302,11 +282,17 @@ static int get_ref_states(const struct ref *remote_refs, struct ref_states *stat else string_list_append(abbrev_branch(ref->name), &states->tracked); } + stale_refs = get_stale_heads(states->remote, fetch_map); + for (ref = stale_refs; ref; ref = ref->next) { + struct string_list_item *item = + string_list_append(abbrev_branch(ref->name), &states->stale); + item->util = xstrdup(ref->name); + } + free_refs(stale_refs); free_refs(fetch_map); sort_string_list(&states->new); sort_string_list(&states->tracked); - for_each_ref(handle_one_branch, states); sort_string_list(&states->stale); return 0; @@ -784,7 +770,7 @@ static void clear_push_info(void *util, const char *string) static void free_remote_ref_states(struct ref_states *states) { string_list_clear(&states->new, 0); - string_list_clear(&states->stale, 0); + string_list_clear(&states->stale, 1); string_list_clear(&states->tracked, 0); string_list_clear(&states->heads, 0); string_list_clear_func(&states->push, clear_push_info); @@ -1213,94 +1199,62 @@ static int prune_remote(const char *remote, int dry_run) printf(" * [%s] %s\n", dry_run ? "would prune" : "pruned", abbrev_ref(refname, "refs/remotes/")); - warn_dangling_symref(dangling_msg, refname); + warn_dangling_symref(stdout, dangling_msg, refname); } free_remote_ref_states(&states); return result; } -static int get_one_remote_for_update(struct remote *remote, void *priv) +static int get_remote_default(const char *key, const char *value, void *priv) { - struct string_list *list = priv; - if (!remote->skip_default_update) - string_list_append(remote->name, list); - return 0; -} - -static struct remote_group { - const char *name; - struct string_list *list; -} remote_group; - -static int get_remote_group(const char *key, const char *value, void *num_hits) -{ - if (!prefixcmp(key, "remotes.") && - !strcmp(key + 8, remote_group.name)) { - /* split list by white space */ - int space = strcspn(value, " \t\n"); - while (*value) { - if (space > 1) { - string_list_append(xstrndup(value, space), - remote_group.list); - ++*((int *)num_hits); - } - value += space + (value[space] != '\0'); - space = strcspn(value, " \t\n"); - } + if (strcmp(key, "remotes.default") == 0) { + int *found = priv; + *found = 1; } - return 0; } static int update(int argc, const char **argv) { - int i, result = 0, prune = 0; - struct string_list list = { NULL, 0, 0, 0 }; - static const char *default_argv[] = { NULL, "default", NULL }; + int i, prune = 0; struct option options[] = { OPT_BOOLEAN('p', "prune", &prune, "prune remotes after fetching"), OPT_END() }; + const char **fetch_argv; + int fetch_argc = 0; + int default_defined = 0; + + fetch_argv = xmalloc(sizeof(char *) * (argc+5)); argc = parse_options(argc, argv, NULL, options, builtin_remote_update_usage, PARSE_OPT_KEEP_ARGV0); + + fetch_argv[fetch_argc++] = "fetch"; + + if (prune) + fetch_argv[fetch_argc++] = "--prune"; + if (verbose) + fetch_argv[fetch_argc++] = "-v"; if (argc < 2) { - argc = 2; - argv = default_argv; + fetch_argv[fetch_argc++] = "default"; + } else { + fetch_argv[fetch_argc++] = "--multiple"; + for (i = 1; i < argc; i++) + fetch_argv[fetch_argc++] = argv[i]; } - remote_group.list = &list; - for (i = 1; i < argc; i++) { - int groups_found = 0; - remote_group.name = argv[i]; - result = git_config(get_remote_group, &groups_found); - if (!groups_found && (i != 1 || strcmp(argv[1], "default"))) { - struct remote *remote; - if (!remote_is_configured(argv[i])) - die("No such remote or remote group: %s", - argv[i]); - remote = remote_get(argv[i]); - string_list_append(remote->name, remote_group.list); - } + if (strcmp(fetch_argv[fetch_argc-1], "default") == 0) { + git_config(get_remote_default, &default_defined); + if (!default_defined) + fetch_argv[fetch_argc-1] = "--all"; } - if (!result && !list.nr && argc == 2 && !strcmp(argv[1], "default")) - result = for_each_remote(get_one_remote_for_update, &list); + fetch_argv[fetch_argc] = NULL; - for (i = 0; i < list.nr; i++) { - int err = fetch_remote(list.items[i].string); - result |= err; - if (!err && prune) - result |= prune_remote(list.items[i].string, 0); - } - - /* all names were strdup()ed or strndup()ed */ - list.strdup_strings = 1; - string_list_clear(&list, 0); - - return result; + return run_command_v_opt(fetch_argv, RUN_GIT_CMD); } static int get_one_entry(struct remote *remote, void *priv) diff --git a/builtin-rev-list.c b/builtin-rev-list.c index ac1136a3f5..91b604289d 100644 --- a/builtin-rev-list.c +++ b/builtin-rev-list.c @@ -306,7 +306,6 @@ int cmd_rev_list(int argc, const char **argv, const char *prefix) struct rev_info revs; struct rev_list_info info; int i; - int read_from_stdin = 0; int bisect_list = 0; int bisect_show_vars = 0; int bisect_find_all = 0; @@ -351,12 +350,6 @@ int cmd_rev_list(int argc, const char **argv, const char *prefix) bisect_show_vars = 1; continue; } - if (!strcmp(arg, "--stdin")) { - if (read_from_stdin++) - die("--stdin given twice?"); - read_revisions_from_stdin(&revs); - continue; - } usage(rev_list_usage); } diff --git a/builtin-send-pack.c b/builtin-send-pack.c index d26997bcfe..8fffdbf200 100644 --- a/builtin-send-pack.c +++ b/builtin-send-pack.c @@ -40,7 +40,7 @@ static int pack_objects(int fd, struct ref *refs, struct extra_have_objects *ext */ const char *argv[] = { "pack-objects", - "--all-progress", + "--all-progress-implied", "--revs", "--stdout", NULL, diff --git a/builtin-shortlog.c b/builtin-shortlog.c index 8aa63c7857..b3b055f68c 100644 --- a/builtin-shortlog.c +++ b/builtin-shortlog.c @@ -139,8 +139,12 @@ static void read_from_stdin(struct shortlog *log) void shortlog_add_commit(struct shortlog *log, struct commit *commit) { const char *author = NULL, *buffer; + struct strbuf buf = STRBUF_INIT; + struct strbuf ufbuf = STRBUF_INIT; + struct pretty_print_context ctx = {0}; - buffer = commit->buffer; + pretty_print_commit(CMIT_FMT_RAW, commit, &buf, &ctx); + buffer = buf.buf; while (*buffer && *buffer != '\n') { const char *eol = strchr(buffer, '\n'); @@ -157,20 +161,19 @@ void shortlog_add_commit(struct shortlog *log, struct commit *commit) die("Missing author: %s", sha1_to_hex(commit->object.sha1)); if (log->user_format) { - struct strbuf buf = STRBUF_INIT; struct pretty_print_context ctx = {0}; ctx.abbrev = DEFAULT_ABBREV; ctx.subject = ""; ctx.after_subject = ""; ctx.date_mode = DATE_NORMAL; - pretty_print_commit(CMIT_FMT_USERFORMAT, commit, &buf, &ctx); - insert_one_record(log, author, buf.buf); - strbuf_release(&buf); - return; - } - if (*buffer) + pretty_print_commit(CMIT_FMT_USERFORMAT, commit, &ufbuf, &ctx); + buffer = ufbuf.buf; + } else if (*buffer) { buffer++; + } insert_one_record(log, author, !*buffer ? "" : buffer); + strbuf_release(&ufbuf); + strbuf_release(&buf); } static void get_from_rev(struct rev_info *rev, struct shortlog *log) diff --git a/bundle.c b/bundle.c index df95e151e2..ff97adcb89 100644 --- a/bundle.c +++ b/bundle.c @@ -204,7 +204,6 @@ int create_bundle(struct bundle_header *header, const char *path, int i, ref_count = 0; char buffer[1024]; struct rev_info revs; - int read_from_stdin = 0; struct child_process rls; FILE *rls_fout; @@ -256,15 +255,8 @@ int create_bundle(struct bundle_header *header, const char *path, /* write references */ argc = setup_revisions(argc, argv, &revs, NULL); - for (i = 1; i < argc; i++) { - if (!strcmp(argv[i], "--stdin")) { - if (read_from_stdin++) - die("--stdin given twice?"); - read_revisions_from_stdin(&revs); - continue; - } - return error("unrecognized argument: %s'", argv[i]); - } + if (argc > 1) + return error("unrecognized argument: %s'", argv[1]); object_array_remove_duplicates(&revs.pending); @@ -351,7 +343,7 @@ int create_bundle(struct bundle_header *header, const char *path, /* write pack */ argv_pack[0] = "pack-objects"; - argv_pack[1] = "--all-progress"; + argv_pack[1] = "--all-progress-implied"; argv_pack[2] = "--stdout"; argv_pack[3] = "--thin"; argv_pack[4] = NULL; diff --git a/cache.h b/cache.h index 14c88e65c3..bf468e5235 100644 --- a/cache.h +++ b/cache.h @@ -369,6 +369,7 @@ static inline enum object_type object_type(unsigned int mode) #define CONFIG_ENVIRONMENT "GIT_CONFIG" #define EXEC_PATH_ENVIRONMENT "GIT_EXEC_PATH" #define CEILING_DIRECTORIES_ENVIRONMENT "GIT_CEILING_DIRECTORIES" +#define NO_REPLACE_OBJECTS_ENVIRONMENT "GIT_NO_REPLACE_OBJECTS" #define GITATTRIBUTES_FILE ".gitattributes" #define INFOATTRIBUTES_FILE "info/attributes" #define ATTRIBUTE_MACRO_PREFIX "[attr]" diff --git a/color.h b/color.h index 7d8da6fe22..3cb4b7fc89 100644 --- a/color.h +++ b/color.h @@ -4,6 +4,11 @@ /* "\033[1;38;5;2xx;48;5;2xxm\0" is 23 bytes */ #define COLOR_MAXLEN 24 +/* + * IMPORTANT: Due to the way these color codes are emulated on Windows, + * write them only using printf(), fprintf(), and fputs(). In particular, + * do not use puts() or write(). + */ #define GIT_COLOR_NORMAL "" #define GIT_COLOR_RESET "\033[m" #define GIT_COLOR_BOLD "\033[1m" diff --git a/combine-diff.c b/combine-diff.c index 5b63af1eeb..61626912e3 100644 --- a/combine-diff.c +++ b/combine-diff.c @@ -524,6 +524,7 @@ static void dump_sline(struct sline *sline, unsigned long cnt, int num_parent, int i; unsigned long lno = 0; const char *c_frag = diff_get_color(use_color, DIFF_FRAGINFO); + const char *c_func = diff_get_color(use_color, DIFF_FUNCINFO); const char *c_new = diff_get_color(use_color, DIFF_FILE_NEW); const char *c_old = diff_get_color(use_color, DIFF_FILE_OLD); const char *c_plain = diff_get_color(use_color, DIFF_PLAIN); @@ -588,7 +589,9 @@ static void dump_sline(struct sline *sline, unsigned long cnt, int num_parent, comment_end = i; } if (comment_end) - putchar(' '); + printf("%s%s %s%s", c_reset, + c_plain, c_reset, + c_func); for (i = 0; i < comment_end; i++) putchar(hunk_comment[i]); } diff --git a/compat/bswap.h b/compat/bswap.h index 279e0b48b1..f3b8c44181 100644 --- a/compat/bswap.h +++ b/compat/bswap.h @@ -24,7 +24,7 @@ static inline uint32_t default_swab32(uint32_t val) if (__builtin_constant_p(x)) { \ __res = default_swab32(x); \ } else { \ - __asm__("bswap %0" : "=r" (__res) : "0" (x)); \ + __asm__("bswap %0" : "=r" (__res) : "0" ((uint32_t)(x))); \ } \ __res; }) diff --git a/compat/mingw.c b/compat/mingw.c index 15fe33eaa0..0d73f15fa8 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -903,19 +903,195 @@ char **make_augmented_environ(const char *const *vars) return env; } -/* this is the first function to call into WS_32; initialize it */ -#undef gethostbyname -struct hostent *mingw_gethostbyname(const char *host) +/* + * Note, this isn't a complete replacement for getaddrinfo. It assumes + * that service contains a numerical port, or that it it is null. It + * does a simple search using gethostbyname, and returns one IPv4 host + * if one was found. + */ +static int WSAAPI getaddrinfo_stub(const char *node, const char *service, + const struct addrinfo *hints, + struct addrinfo **res) +{ + struct hostent *h = gethostbyname(node); + struct addrinfo *ai; + struct sockaddr_in *sin; + + if (!h) + return WSAGetLastError(); + + ai = xmalloc(sizeof(struct addrinfo)); + *res = ai; + ai->ai_flags = 0; + ai->ai_family = AF_INET; + ai->ai_socktype = hints->ai_socktype; + switch (hints->ai_socktype) { + case SOCK_STREAM: + ai->ai_protocol = IPPROTO_TCP; + break; + case SOCK_DGRAM: + ai->ai_protocol = IPPROTO_UDP; + break; + default: + ai->ai_protocol = 0; + break; + } + ai->ai_addrlen = sizeof(struct sockaddr_in); + ai->ai_canonname = strdup(h->h_name); + + sin = xmalloc(ai->ai_addrlen); + memset(sin, 0, ai->ai_addrlen); + sin->sin_family = AF_INET; + if (service) + sin->sin_port = htons(atoi(service)); + sin->sin_addr = *(struct in_addr *)h->h_addr; + ai->ai_addr = (struct sockaddr *)sin; + ai->ai_next = 0; + return 0; +} + +static void WSAAPI freeaddrinfo_stub(struct addrinfo *res) +{ + free(res->ai_canonname); + free(res->ai_addr); + free(res); +} + +static int WSAAPI getnameinfo_stub(const struct sockaddr *sa, socklen_t salen, + char *host, DWORD hostlen, + char *serv, DWORD servlen, int flags) +{ + const struct sockaddr_in *sin = (const struct sockaddr_in *)sa; + if (sa->sa_family != AF_INET) + return EAI_FAMILY; + if (!host && !serv) + return EAI_NONAME; + + if (host && hostlen > 0) { + struct hostent *ent = NULL; + if (!(flags & NI_NUMERICHOST)) + ent = gethostbyaddr((const char *)&sin->sin_addr, + sizeof(sin->sin_addr), AF_INET); + + if (ent) + snprintf(host, hostlen, "%s", ent->h_name); + else if (flags & NI_NAMEREQD) + return EAI_NONAME; + else + snprintf(host, hostlen, "%s", inet_ntoa(sin->sin_addr)); + } + + if (serv && servlen > 0) { + struct servent *ent = NULL; + if (!(flags & NI_NUMERICSERV)) + ent = getservbyport(sin->sin_port, + flags & NI_DGRAM ? "udp" : "tcp"); + + if (ent) + snprintf(serv, servlen, "%s", ent->s_name); + else + snprintf(serv, servlen, "%d", ntohs(sin->sin_port)); + } + + return 0; +} + +static HMODULE ipv6_dll = NULL; +static void (WSAAPI *ipv6_freeaddrinfo)(struct addrinfo *res); +static int (WSAAPI *ipv6_getaddrinfo)(const char *node, const char *service, + const struct addrinfo *hints, + struct addrinfo **res); +static int (WSAAPI *ipv6_getnameinfo)(const struct sockaddr *sa, socklen_t salen, + char *host, DWORD hostlen, + char *serv, DWORD servlen, int flags); +/* + * gai_strerror is an inline function in the ws2tcpip.h header, so we + * don't need to try to load that one dynamically. + */ + +static void socket_cleanup(void) +{ + WSACleanup(); + if (ipv6_dll) + FreeLibrary(ipv6_dll); + ipv6_dll = NULL; + ipv6_freeaddrinfo = freeaddrinfo_stub; + ipv6_getaddrinfo = getaddrinfo_stub; + ipv6_getnameinfo = getnameinfo_stub; +} + +static void ensure_socket_initialization(void) { WSADATA wsa; + static int initialized = 0; + const char *libraries[] = { "ws2_32.dll", "wship6.dll", NULL }; + const char **name; + + if (initialized) + return; if (WSAStartup(MAKEWORD(2,2), &wsa)) die("unable to initialize winsock subsystem, error %d", WSAGetLastError()); - atexit((void(*)(void)) WSACleanup); + + for (name = libraries; *name; name++) { + ipv6_dll = LoadLibrary(*name); + if (!ipv6_dll) + continue; + + ipv6_freeaddrinfo = (void (WSAAPI *)(struct addrinfo *)) + GetProcAddress(ipv6_dll, "freeaddrinfo"); + ipv6_getaddrinfo = (int (WSAAPI *)(const char *, const char *, + const struct addrinfo *, + struct addrinfo **)) + GetProcAddress(ipv6_dll, "getaddrinfo"); + ipv6_getnameinfo = (int (WSAAPI *)(const struct sockaddr *, + socklen_t, char *, DWORD, + char *, DWORD, int)) + GetProcAddress(ipv6_dll, "getnameinfo"); + if (!ipv6_freeaddrinfo || !ipv6_getaddrinfo || !ipv6_getnameinfo) { + FreeLibrary(ipv6_dll); + ipv6_dll = NULL; + } else + break; + } + if (!ipv6_freeaddrinfo || !ipv6_getaddrinfo || !ipv6_getnameinfo) { + ipv6_freeaddrinfo = freeaddrinfo_stub; + ipv6_getaddrinfo = getaddrinfo_stub; + ipv6_getnameinfo = getnameinfo_stub; + } + + atexit(socket_cleanup); + initialized = 1; +} + +#undef gethostbyname +struct hostent *mingw_gethostbyname(const char *host) +{ + ensure_socket_initialization(); return gethostbyname(host); } +void mingw_freeaddrinfo(struct addrinfo *res) +{ + ipv6_freeaddrinfo(res); +} + +int mingw_getaddrinfo(const char *node, const char *service, + const struct addrinfo *hints, struct addrinfo **res) +{ + ensure_socket_initialization(); + return ipv6_getaddrinfo(node, service, hints, res); +} + +int mingw_getnameinfo(const struct sockaddr *sa, socklen_t salen, + char *host, DWORD hostlen, char *serv, DWORD servlen, + int flags) +{ + ensure_socket_initialization(); + return ipv6_getnameinfo(sa, salen, host, hostlen, serv, servlen, flags); +} + int mingw_socket(int domain, int type, int protocol) { int sockfd; diff --git a/compat/mingw.h b/compat/mingw.h index 51993ef114..b3d299f5bc 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -1,4 +1,5 @@ #include +#include /* * things that are not available in header files @@ -178,6 +179,18 @@ char *mingw_getenv(const char *name); struct hostent *mingw_gethostbyname(const char *host); #define gethostbyname mingw_gethostbyname +void mingw_freeaddrinfo(struct addrinfo *res); +#define freeaddrinfo mingw_freeaddrinfo + +int mingw_getaddrinfo(const char *node, const char *service, + const struct addrinfo *hints, struct addrinfo **res); +#define getaddrinfo mingw_getaddrinfo + +int mingw_getnameinfo(const struct sockaddr *sa, socklen_t salen, + char *host, DWORD hostlen, char *serv, DWORD servlen, + int flags); +#define getnameinfo mingw_getnameinfo + int mingw_socket(int domain, int type, int protocol); #define socket mingw_socket diff --git a/connect.c b/connect.c index 839a103705..db965c9982 100644 --- a/connect.c +++ b/connect.c @@ -609,6 +609,7 @@ struct child_process *git_connect(int fd[2], const char *url_orig, GIT_WORK_TREE_ENVIRONMENT, GRAFT_ENVIRONMENT, INDEX_ENVIRONMENT, + NO_REPLACE_OBJECTS_ENVIRONMENT, NULL }; conn->env = env; diff --git a/diff.c b/diff.c index 0d7f5ea4a8..d952686926 100644 --- a/diff.c +++ b/diff.c @@ -39,6 +39,7 @@ static char diff_colors[][COLOR_MAXLEN] = { GIT_COLOR_GREEN, /* NEW */ GIT_COLOR_YELLOW, /* COMMIT */ GIT_COLOR_BG_RED, /* WHITESPACE */ + GIT_COLOR_NORMAL, /* FUNCINFO */ }; static void diff_filespec_load_driver(struct diff_filespec *one); @@ -60,6 +61,8 @@ static int parse_diff_color_slot(const char *var, int ofs) return DIFF_COMMIT; if (!strcasecmp(var+ofs, "whitespace")) return DIFF_WHITESPACE; + if (!strcasecmp(var+ofs, "func")) + return DIFF_FUNCINFO; die("bad config variable '%s'", var); } @@ -295,12 +298,13 @@ static void emit_line_0(FILE *file, const char *set, const char *reset, nofirst = 0; } - fputs(set, file); - - if (!nofirst) - fputc(first, file); - fwrite(line, len, 1, file); - fputs(reset, file); + if (len || !nofirst) { + fputs(set, file); + if (!nofirst) + fputc(first, file); + fwrite(line, len, 1, file); + fputs(reset, file); + } if (has_trailing_carriage_return) fputc('\r', file); if (has_trailing_newline) @@ -344,6 +348,42 @@ static void emit_add_line(const char *reset, } } +static void emit_hunk_header(struct emit_callback *ecbdata, + const char *line, int len) +{ + const char *plain = diff_get_color(ecbdata->color_diff, DIFF_PLAIN); + const char *frag = diff_get_color(ecbdata->color_diff, DIFF_FRAGINFO); + const char *func = diff_get_color(ecbdata->color_diff, DIFF_FUNCINFO); + const char *reset = diff_get_color(ecbdata->color_diff, DIFF_RESET); + static const char atat[2] = { '@', '@' }; + const char *cp, *ep; + + /* + * As a hunk header must begin with "@@ -, + @@", + * it always is at least 10 bytes long. + */ + if (len < 10 || + memcmp(line, atat, 2) || + !(ep = memmem(line + 2, len - 2, atat, 2))) { + emit_line(ecbdata->file, plain, reset, line, len); + return; + } + ep += 2; /* skip over @@ */ + + /* The hunk header in fraginfo color */ + emit_line(ecbdata->file, frag, reset, line, ep - line); + + /* blank before the func header */ + for (cp = ep; ep - line < len; ep++) + if (*ep != ' ' && *ep != '\t') + break; + if (ep != cp) + emit_line(ecbdata->file, plain, reset, cp, ep - cp); + + if (ep < line + len) + emit_line(ecbdata->file, func, reset, ep, line + len - ep); +} + static struct diff_tempfile *claim_diff_tempfile(void) { int i; for (i = 0; i < ARRAY_SIZE(diff_temp); i++) @@ -781,9 +821,7 @@ static void fn_out_consume(void *priv, char *line, unsigned long len) diff_words_flush(ecbdata); len = sane_truncate_line(ecbdata, line, len); find_lno(line, ecbdata); - emit_line(ecbdata->file, - diff_get_color(ecbdata->color_diff, DIFF_FRAGINFO), - reset, line, len); + emit_hunk_header(ecbdata, line, len); if (line[len-1] != '\n') putc('\n', ecbdata->file); return; diff --git a/diff.h b/diff.h index 2740421cfe..15fcecdecd 100644 --- a/diff.h +++ b/diff.h @@ -130,6 +130,7 @@ enum color_diff { DIFF_FILE_NEW = 5, DIFF_COMMIT = 6, DIFF_WHITESPACE = 7, + DIFF_FUNCINFO = 8, }; const char *diff_get_color(int diff_use_color, enum color_diff ix); #define diff_get_color_opt(o, ix) \ diff --git a/environment.c b/environment.c index 571ab56b7e..5171d9f9a4 100644 --- a/environment.c +++ b/environment.c @@ -84,6 +84,8 @@ static void setup_git_env(void) git_graft_file = getenv(GRAFT_ENVIRONMENT); if (!git_graft_file) git_graft_file = git_pathdup("info/grafts"); + if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT)) + read_replace_refs = 0; } int is_bare_repository(void) diff --git a/git-am.sh b/git-am.sh index 151512afd3..4838cdb9ed 100755 --- a/git-am.sh +++ b/git-am.sh @@ -205,7 +205,7 @@ check_patch_format () { # and see if it looks like that they all begin with the # header field names... sed -n -e '/^$/q' -e '/^[ ]/d' -e p "$1" | - LC_ALL=C egrep -v '^[!-9;-~]+:' >/dev/null || + sane_egrep -v '^[!-9;-~]+:' >/dev/null || patch_format=mbox fi } < "$1" || clean_abort @@ -561,7 +561,7 @@ do stop_here $this # skip pine's internal folder data - grep '^Author: Mail System Internal Data$' \ + sane_grep '^Author: Mail System Internal Data$' \ <"$dotest"/info >/dev/null && go_next && continue @@ -577,11 +577,12 @@ do git cat-file commit "$commit" | sed -e '1,/^$/d' >"$dotest/msg-clean" else - SUBJECT="$(sed -n '/^Subject/ s/Subject: //p' "$dotest/info")" - case "$keep_subject" in -k) SUBJECT="[PATCH] $SUBJECT" ;; esac - - (printf '%s\n\n' "$SUBJECT"; cat "$dotest/msg") | - git stripspace > "$dotest/msg-clean" + { + sed -n '/^Subject/ s/Subject: //p' "$dotest/info" + echo + cat "$dotest/msg" + } | + git stripspace > "$dotest/msg-clean" fi ;; esac diff --git a/git-bisect.sh b/git-bisect.sh index 8b3c5858a9..6e2acb8ef2 100755 --- a/git-bisect.sh +++ b/git-bisect.sh @@ -300,8 +300,7 @@ bisect_visualize() { esac fi - not=$(git for-each-ref --format='%(refname)' "refs/bisect/good-*") - eval '"$@"' refs/bisect/bad --not $not -- $(cat "$GIT_DIR/BISECT_NAMES") + eval '"$@"' --bisect -- $(cat "$GIT_DIR/BISECT_NAMES") } bisect_reset() { @@ -393,7 +392,7 @@ bisect_run () { cat "$GIT_DIR/BISECT_RUN" - if grep "first bad commit could be any of" "$GIT_DIR/BISECT_RUN" \ + if sane_grep "first bad commit could be any of" "$GIT_DIR/BISECT_RUN" \ > /dev/null; then echo >&2 "bisect run cannot continue any more" exit $res @@ -405,7 +404,7 @@ bisect_run () { exit $res fi - if grep "is the first bad commit" "$GIT_DIR/BISECT_RUN" > /dev/null; then + if sane_grep "is the first bad commit" "$GIT_DIR/BISECT_RUN" > /dev/null; then echo "bisect run success" exit 0; fi diff --git a/git-filter-branch.sh b/git-filter-branch.sh index 6b8b6a4f26..cb9d2022cc 100755 --- a/git-filter-branch.sh +++ b/git-filter-branch.sh @@ -473,7 +473,7 @@ if [ "$filter_tag_name" ]; then git mktag) || die "Could not create new tag object for $ref" if git cat-file tag "$ref" | \ - grep '^-----BEGIN PGP SIGNATURE-----' >/dev/null 2>&1 + sane_grep '^-----BEGIN PGP SIGNATURE-----' >/dev/null 2>&1 then warn "gpg signature stripped from tag object $sha1t" fi diff --git a/git-instaweb.sh b/git-instaweb.sh index 622a5f0eb2..b8e6456208 100755 --- a/git-instaweb.sh +++ b/git-instaweb.sh @@ -41,7 +41,7 @@ resolve_full_httpd () { case "$httpd" in *apache2*|*lighttpd*) # ensure that the apache2/lighttpd command ends with "-f" - if ! echo "$httpd" | grep -- '-f *$' >/dev/null 2>&1 + if ! echo "$httpd" | sane_grep -- '-f *$' >/dev/null 2>&1 then httpd="$httpd -f" fi @@ -73,6 +73,11 @@ resolve_full_httpd () { } start_httpd () { + if test -f "$fqgitdir/pid"; then + say "Instance already running. Restarting..." + stop_httpd + fi + # here $httpd should have a meaningful value resolve_full_httpd @@ -297,8 +302,8 @@ EOF # check to see if Dennis Stosberg's mod_perl compatibility patch # (<20060621130708.Gcbc6e5c@leonov.stosberg.net>) has been applied - if test -f "$module_path/mod_perl.so" && grep 'MOD_PERL' \ - "$GIT_DIR/gitweb/gitweb.cgi" >/dev/null + if test -f "$module_path/mod_perl.so" && + sane_grep 'MOD_PERL' "$GIT_DIR/gitweb/gitweb.cgi" >/dev/null then # favor mod_perl if available cat >> "$conf" </dev/null 2>&1 || \ + $list_mods | sane_grep 'mod_cgi\.c' >/dev/null 2>&1 || \ if test -f "$module_path/mod_cgi.so" then echo "LoadModule cgi_module $module_path/mod_cgi.so" >> "$conf" @@ -389,8 +394,15 @@ gitweb_css () { EOFGITWEB } +gitweb_js () { + cat > "$1" <<\EOFGITWEB +@@GITWEB_JS@@ +EOFGITWEB +} + gitweb_cgi "$GIT_DIR/gitweb/gitweb.cgi" gitweb_css "$GIT_DIR/gitweb/gitweb.css" +gitweb_js "$GIT_DIR/gitweb/gitweb.js" case "$httpd" in *lighttpd*) diff --git a/git-mergetool--lib.sh b/git-mergetool--lib.sh index f7c571e73c..5b6278572a 100644 --- a/git-mergetool--lib.sh +++ b/git-mergetool--lib.sh @@ -338,15 +338,14 @@ guess_merge_tool () { fi tools="$tools gvimdiff diffuse ecmerge p4merge araxis" fi - if echo "${VISUAL:-$EDITOR}" | grep emacs > /dev/null 2>&1; then - # $EDITOR is emacs so add emerge as a candidate - tools="$tools emerge vimdiff" - elif echo "${VISUAL:-$EDITOR}" | grep vim > /dev/null 2>&1; then - # $EDITOR is vim so add vimdiff as a candidate + case "${VISUAL:-$EDITOR}" in + *vim*) tools="$tools vimdiff emerge" - else + ;; + *) tools="$tools emerge vimdiff" - fi + ;; + esac echo >&2 "merge tool candidates: $tools" # Loop over each candidate and stop when a valid merge tool is found. diff --git a/git-pull.sh b/git-pull.sh index bfeb4a0ff6..502af1a9c4 100755 --- a/git-pull.sh +++ b/git-pull.sh @@ -216,7 +216,7 @@ fi merge_name=$(git fmt-merge-msg $log_arg <"$GIT_DIR/FETCH_HEAD") || exit test true = "$rebase" && - exec git-rebase $diffstat $strategy_args --onto $merge_head \ + exec git rebase $diffstat $strategy_args --onto $merge_head \ ${oldremoteref:-$merge_head} -exec git-merge $diffstat $no_commit $squash $no_ff $ff_only $log_arg $strategy_args \ - "$merge_name" HEAD $merge_head $verbosity +exec git merge $verbosity $diffstat $no_commit $squash $no_ff $ff_only $log_arg $strategy_args \ + -m "$merge_name" $merge_head diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 27daaa9ded..0bd3bf78b8 100755 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -106,8 +106,8 @@ mark_action_done () { sed -e 1q < "$TODO" >> "$DONE" sed -e 1d < "$TODO" >> "$TODO".new mv -f "$TODO".new "$TODO" - count=$(grep -c '^[^#]' < "$DONE") - total=$(($count+$(grep -c '^[^#]' < "$TODO"))) + count=$(sane_grep -c '^[^#]' < "$DONE") + total=$(($count+$(sane_grep -c '^[^#]' < "$TODO"))) if test "$last_count" != "$count" then last_count=$count @@ -147,7 +147,7 @@ die_abort () { } has_action () { - grep '^[^#]' "$1" >/dev/null + sane_grep '^[^#]' "$1" >/dev/null } pick_one () { @@ -744,7 +744,7 @@ first and then run 'git rebase --continue' again." git rev-list $REVISIONS | while read rev do - if test -f "$REWRITTEN"/$rev -a "$(grep "$rev" "$DOTEST"/not-cherry-picks)" = "" + if test -f "$REWRITTEN"/$rev -a "$(sane_grep "$rev" "$DOTEST"/not-cherry-picks)" = "" then # Use -f2 because if rev-list is telling us this commit is # not worthwhile, we don't want to track its multiple heads, @@ -752,7 +752,7 @@ first and then run 'git rebase --continue' again." # be rebasing on top of it git rev-list --parents -1 $rev | cut -d' ' -s -f2 > "$DROPPED"/$rev short=$(git rev-list -1 --abbrev-commit --abbrev=7 $rev) - grep -v "^[a-z][a-z]* $short" <"$TODO" > "${TODO}2" ; mv "${TODO}2" "$TODO" + sane_grep -v "^[a-z][a-z]* $short" <"$TODO" > "${TODO}2" ; mv "${TODO}2" "$TODO" rm "$REWRITTEN"/$rev fi done diff --git a/git-rebase.sh b/git-rebase.sh index 6830e1627d..b121f4537c 100755 --- a/git-rebase.sh +++ b/git-rebase.sh @@ -467,7 +467,7 @@ orig_head=$branch mb=$(git merge-base "$onto" "$branch") if test "$upstream" = "$onto" && test "$mb" = "$onto" && # linear history? - ! (git rev-list --parents "$onto".."$branch" | grep " .* ") > /dev/null + ! (git rev-list --parents "$onto".."$branch" | sane_grep " .* ") > /dev/null then if test -z "$force_rebase" then diff --git a/git-send-email.perl b/git-send-email.perl index 4f5da4ecf2..319b535671 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -187,9 +187,11 @@ my ($identity, $aliasfiletype, @alias_files, @smtp_host_parts); my ($validate, $confirm); my (@suppress_cc); +my $not_set_by_user = "true but not set by the user"; + my %config_bool_settings = ( "thread" => [\$thread, 1], - "chainreplyto" => [\$chain_reply_to, 1], + "chainreplyto" => [\$chain_reply_to, $not_set_by_user], "suppressfrom" => [\$suppress_from, undef], "signedoffbycc" => [\$signed_off_by_cc, undef], "signedoffcc" => [\$signed_off_by_cc, undef], # Deprecated @@ -214,6 +216,19 @@ my %config_settings = ( "from" => \$sender, ); +# Help users prepare for 1.7.0 +sub chain_reply_to { + if (defined $chain_reply_to && + $chain_reply_to eq $not_set_by_user) { + print STDERR + "In git 1.7.0, the default will be changed to --no-chain-reply-to\n" . + "Set sendemail.chainreplyto configuration variable to true if\n" . + "you want to keep --chain-reply-to as your default.\n"; + $chain_reply_to = 1; + } + return $chain_reply_to; +} + # Handle Uncouth Termination sub signal_handler { @@ -862,7 +877,9 @@ X-Mailer: git-send-email $gitversion my @sendmail_parameters = ('-i', @recipients); my $raw_from = $sanitized_sender; - $raw_from = $envelope_sender if (defined $envelope_sender); + if (defined $envelope_sender && $envelope_sender ne "auto") { + $raw_from = $envelope_sender; + } $raw_from = extract_valid_address($raw_from); unshift (@sendmail_parameters, '-f', $raw_from) if(defined $envelope_sender); @@ -1157,7 +1174,7 @@ foreach my $t (@files) { # set up for the next message if ($thread && $message_was_sent && - ($chain_reply_to || !defined $reply_to || length($reply_to) == 0)) { + (chain_reply_to() || !defined $reply_to || length($reply_to) == 0)) { $reply_to = $message_id; if (length $references > 0) { $references .= "\n $message_id"; diff --git a/git-sh-setup.sh b/git-sh-setup.sh index 99cceeb858..dfcb8078f5 100755 --- a/git-sh-setup.sh +++ b/git-sh-setup.sh @@ -107,6 +107,14 @@ git_editor() { eval "$GIT_EDITOR" '"$@"' } +sane_grep () { + GREP_OPTIONS= LC_ALL=C grep "$@" +} + +sane_egrep () { + GREP_OPTIONS= LC_ALL=C egrep "$@" +} + is_bare_repository () { git rev-parse --is-bare-repository } diff --git a/git-submodule.sh b/git-submodule.sh index 850d4235a0..77d223292c 100755 --- a/git-submodule.sh +++ b/git-submodule.sh @@ -57,7 +57,7 @@ resolve_relative_url () # module_list() { - git ls-files --error-unmatch --stage -- "$@" | grep '^160000 ' + git ls-files --error-unmatch --stage -- "$@" | sane_grep '^160000 ' } # @@ -572,7 +572,7 @@ cmd_summary() { cd_to_toplevel # Get modified modules cared by user modules=$(git $diff_cmd $cached --raw $head -- "$@" | - egrep '^:([0-7]* )?160000' | + sane_egrep '^:([0-7]* )?160000' | while read mod_src mod_dst sha1_src sha1_dst status name do # Always show modules deleted or type-changed (blob<->module) @@ -586,7 +586,7 @@ cmd_summary() { test -z "$modules" && return git $diff_cmd $cached --raw $head -- $modules | - egrep '^:([0-7]* )?160000' | + sane_egrep '^:([0-7]* )?160000' | cut -c2- | while read mod_src mod_dst sha1_src sha1_dst status name do diff --git a/git.c b/git.c index 743ee57100..11544cdb40 100644 --- a/git.c +++ b/git.c @@ -89,6 +89,9 @@ static int handle_options(const char ***argv, int *argc, int *envchanged) *envchanged = 1; } else if (!strcmp(cmd, "--no-replace-objects")) { read_replace_refs = 0; + setenv(NO_REPLACE_OBJECTS_ENVIRONMENT, "1", 1); + if (envchanged) + *envchanged = 1; } else if (!strcmp(cmd, "--git-dir")) { if (*argc < 2) { fprintf(stderr, "No directory given for --git-dir.\n" ); diff --git a/gitweb/README b/gitweb/README index 66c6a9391d..b69b0e5042 100644 --- a/gitweb/README +++ b/gitweb/README @@ -92,6 +92,10 @@ You can specify the following configuration variables when building GIT: web browsers that support favicons (website icons) may display them in the browser's URL bar and next to site name in bookmarks). Relative to base URI of gitweb. [Default: git-favicon.png] + * GITWEB_JS + Points to the localtion where you put gitweb.js on your web server + (or to be more generic URI of JavaScript code used by gitweb). + Relative to base URI of gitweb. [Default: gitweb.js] * GITWEB_CONFIG This Perl file will be loaded using 'do' and can be used to override any of the options above as well as some other options -- see the "Runtime diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css index cb3f0baf05..50067f2e0d 100644 --- a/gitweb/gitweb.css +++ b/gitweb/gitweb.css @@ -79,6 +79,13 @@ div.page_footer_text { font-style: italic; } +div#generating_info { + margin: 4px; + font-size: smaller; + text-align: center; + color: #505050; +} + div.page_body { padding: 8px; font-family: monospace; @@ -254,6 +261,11 @@ tr.no-previous td.linenr { font-weight: bold; } +/* for 'blame_incremental', during processing */ +tr.color1 { background-color: #f6fff6; } +tr.color2 { background-color: #f6f6ff; } +tr.color3 { background-color: #fff6f6; } + td { padding: 2px 5px; font-size: 100%; @@ -345,6 +357,17 @@ td.mode { font-family: monospace; } +/* progress of blame_interactive */ +div#progress_bar { + height: 2px; + margin-bottom: -2px; + background-color: #d8d9d0; +} +div#progress_info { + float: right; + text-align: right; +} + /* format of (optional) objects size in 'tree' view */ td.size { font-family: monospace; diff --git a/gitweb/gitweb.js b/gitweb/gitweb.js new file mode 100644 index 0000000000..2a25b7cc47 --- /dev/null +++ b/gitweb/gitweb.js @@ -0,0 +1,870 @@ +// Copyright (C) 2007, Fredrik Kuivinen +// 2007, Petr Baudis +// 2008-2009, Jakub Narebski + +/** + * @fileOverview JavaScript code for gitweb (git web interface). + * @license GPLv2 or later + */ + +/* ============================================================ */ +/* functions for generic gitweb actions and views */ + +/** + * used to check if link has 'js' query parameter already (at end), + * and other reasons to not add 'js=1' param at the end of link + * @constant + */ +var jsExceptionsRe = /[;?]js=[01]$/; + +/** + * Add '?js=1' or ';js=1' to the end of every link in the document + * that doesn't have 'js' query parameter set already. + * + * Links with 'js=1' lead to JavaScript version of given action, if it + * exists (currently there is only 'blame_incremental' for 'blame') + * + * @globals jsExceptionsRe + */ +function fixLinks() { + var allLinks = document.getElementsByTagName("a") || document.links; + for (var i = 0, len = allLinks.length; i < len; i++) { + var link = allLinks[i]; + if (!jsExceptionsRe.test(link)) { // =~ /[;?]js=[01]$/; + link.href += + (link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1'; + } + } +} + + +/* ============================================================ */ + +/* + * This code uses DOM methods instead of (nonstandard) innerHTML + * to modify page. + * + * innerHTML is non-standard IE extension, though supported by most + * browsers; however Firefox up to version 1.5 didn't implement it in + * a strict mode (application/xml+xhtml mimetype). + * + * Also my simple benchmarks show that using elem.firstChild.data = + * 'content' is slightly faster than elem.innerHTML = 'content'. It + * is however more fragile (text element fragment must exists), and + * less feature-rich (we cannot add HTML). + * + * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the + * equivalent using DOM 2 Core is usually shown in comments. + */ + + +/* ============================================================ */ +/* generic utility functions */ + + +/** + * pad number N with nonbreakable spaces on the left, to WIDTH characters + * example: padLeftStr(12, 3, '\u00A0') == '\u00A012' + * ('\u00A0' is nonbreakable space) + * + * @param {Number|String} input: number to pad + * @param {Number} width: visible width of output + * @param {String} str: string to prefix to string, e.g. '\u00A0' + * @returns {String} INPUT prefixed with (WIDTH - INPUT.length) x STR + */ +function padLeftStr(input, width, str) { + var prefix = ''; + + width -= input.toString().length; + while (width > 0) { + prefix += str; + width--; + } + return prefix + input; +} + +/** + * Pad INPUT on the left to SIZE width, using given padding character CH, + * for example padLeft('a', 3, '_') is '__a'. + * + * @param {String} input: input value converted to string. + * @param {Number} width: desired length of output. + * @param {String} ch: single character to prefix to string. + * + * @returns {String} Modified string, at least SIZE length. + */ +function padLeft(input, width, ch) { + var s = input + ""; + while (s.length < width) { + s = ch + s; + } + return s; +} + +/** + * Create XMLHttpRequest object in cross-browser way + * @returns XMLHttpRequest object, or null + */ +function createRequestObject() { + try { + return new XMLHttpRequest(); + } catch (e) {} + try { + return window.createRequest(); + } catch (e) {} + try { + return new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) {} + try { + return new ActiveXObject("Microsoft.XMLHTTP"); + } catch (e) {} + + return null; +} + + +/* ============================================================ */ +/* utility/helper functions (and variables) */ + +var xhr; // XMLHttpRequest object +var projectUrl; // partial query + separator ('?' or ';') + +// 'commits' is an associative map. It maps SHA1s to Commit objects. +var commits = {}; + +/** + * constructor for Commit objects, used in 'blame' + * @class Represents a blamed commit + * @param {String} sha1: SHA-1 identifier of a commit + */ +function Commit(sha1) { + if (this instanceof Commit) { + this.sha1 = sha1; + this.nprevious = 0; /* number of 'previous', effective parents */ + } else { + return new Commit(sha1); + } +} + +/* ............................................................ */ +/* progress info, timing, error reporting */ + +var blamedLines = 0; +var totalLines = '???'; +var div_progress_bar; +var div_progress_info; + +/** + * Detects how many lines does a blamed file have, + * This information is used in progress info + * + * @returns {Number|String} Number of lines in file, or string '...' + */ +function countLines() { + var table = + document.getElementById('blame_table') || + document.getElementsByTagName('table')[0]; + + if (table) { + return table.getElementsByTagName('tr').length - 1; // for header + } else { + return '...'; + } +} + +/** + * update progress info and length (width) of progress bar + * + * @globals div_progress_info, div_progress_bar, blamedLines, totalLines + */ +function updateProgressInfo() { + if (!div_progress_info) { + div_progress_info = document.getElementById('progress_info'); + } + if (!div_progress_bar) { + div_progress_bar = document.getElementById('progress_bar'); + } + if (!div_progress_info && !div_progress_bar) { + return; + } + + var percentage = Math.floor(100.0*blamedLines/totalLines); + + if (div_progress_info) { + div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines + + ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)'; + } + + if (div_progress_bar) { + //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;'); + div_progress_bar.style.width = percentage + '%'; + } +} + + +var t_interval_server = ''; +var cmds_server = ''; +var t0 = new Date(); + +/** + * write how much it took to generate data, and to run script + * + * @globals t0, t_interval_server, cmds_server + */ +function writeTimeInterval() { + var info_time = document.getElementById('generating_time'); + if (!info_time || !t_interval_server) { + return; + } + var t1 = new Date(); + info_time.firstChild.data += ' + (' + + t_interval_server + ' sec server blame_data / ' + + (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)'; + + var info_cmds = document.getElementById('generating_cmd'); + if (!info_time || !cmds_server) { + return; + } + info_cmds.firstChild.data += ' + ' + cmds_server; +} + +/** + * show an error message alert to user within page (in prohress info area) + * @param {String} str: plain text error message (no HTML) + * + * @globals div_progress_info + */ +function errorInfo(str) { + if (!div_progress_info) { + div_progress_info = document.getElementById('progress_info'); + } + if (div_progress_info) { + div_progress_info.className = 'error'; + div_progress_info.firstChild.data = str; + } +} + +/* ............................................................ */ +/* coloring rows during blame_data (git blame --incremental) run */ + +/** + * used to extract N from 'colorN', where N is a number, + * @constant + */ +var colorRe = /\bcolor([0-9]*)\b/; + +/** + * return N if , otherwise return null + * (some browsers require CSS class names to begin with letter) + * + * @param {HTMLElement} tr: table row element to check + * @param {String} tr.className: 'class' attribute of tr element + * @returns {Number|null} N if tr.className == 'colorN', otherwise null + * + * @globals colorRe + */ +function getColorNo(tr) { + if (!tr) { + return null; + } + var className = tr.className; + if (className) { + var match = colorRe.exec(className); + if (match) { + return parseInt(match[1], 10); + } + } + return null; +} + +var colorsFreq = [0, 0, 0]; +/** + * return one of given possible colors (curently least used one) + * example: chooseColorNoFrom(2, 3) returns 2 or 3 + * + * @param {Number[]} arguments: one or more numbers + * assumes that 1 <= arguments[i] <= colorsFreq.length + * @returns {Number} Least used color number from arguments + * @globals colorsFreq + */ +function chooseColorNoFrom() { + // choose the color which is least used + var colorNo = arguments[0]; + for (var i = 1; i < arguments.length; i++) { + if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) { + colorNo = arguments[i]; + } + } + colorsFreq[colorNo-1]++; + return colorNo; +} + +/** + * given two neigbour elements, find color which would be different + * from color of both of neighbours; used to 3-color blame table + * + * @param {HTMLElement} tr_prev + * @param {HTMLElement} tr_next + * @returns {Number} color number N such that + * colorN != tr_prev.className && colorN != tr_next.className + */ +function findColorNo(tr_prev, tr_next) { + var color_prev = getColorNo(tr_prev); + var color_next = getColorNo(tr_next); + + + // neither of neighbours has color set + // THEN we can use any of 3 possible colors + if (!color_prev && !color_next) { + return chooseColorNoFrom(1,2,3); + } + + // either both neighbours have the same color, + // or only one of neighbours have color set + // THEN we can use any color except given + var color; + if (color_prev === color_next) { + color = color_prev; // = color_next; + } else if (!color_prev) { + color = color_next; + } else if (!color_next) { + color = color_prev; + } + if (color) { + return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1); + } + + // neighbours have different colors + // THEN there is only one color left + return (3 - ((color_prev + color_next) % 3)); +} + +/* ............................................................ */ +/* coloring rows like 'blame' after 'blame_data' finishes */ + +/** + * returns true if given row element (tr) is first in commit group + * to be used only after 'blame_data' finishes (after processing) + * + * @param {HTMLElement} tr: table row + * @returns {Boolean} true if TR is first in commit group + */ +function isStartOfGroup(tr) { + return tr.firstChild.className === 'sha1'; +} + +/** + * change colors to use zebra coloring (2 colors) instead of 3 colors + * concatenate neighbour commit groups belonging to the same commit + * + * @globals colorRe + */ +function fixColorsAndGroups() { + var colorClasses = ['light', 'dark']; + var linenum = 1; + var tr, prev_group; + var colorClass = 0; + var table = + document.getElementById('blame_table') || + document.getElementsByTagName('table')[0]; + + while ((tr = document.getElementById('l'+linenum))) { + // index origin is 0, which is table header; start from 1 + //while ((tr = table.rows[linenum])) { // <- it is slower + if (isStartOfGroup(tr, linenum, document)) { + if (prev_group && + prev_group.firstChild.firstChild.href === + tr.firstChild.firstChild.href) { + // we have to concatenate groups + var prev_rows = prev_group.firstChild.rowSpan || 1; + var curr_rows = tr.firstChild.rowSpan || 1; + prev_group.firstChild.rowSpan = prev_rows + curr_rows; + //tr.removeChild(tr.firstChild); + tr.deleteCell(0); // DOM2 HTML way + } else { + colorClass = (colorClass + 1) % 2; + prev_group = tr; + } + } + var tr_class = tr.className; + tr.className = tr_class.replace(colorRe, colorClasses[colorClass]); + linenum++; + } +} + +/* ............................................................ */ +/* time and data */ + +/** + * used to extract hours and minutes from timezone info, e.g '-0900' + * @constant + */ +var tzRe = /^([+-][0-9][0-9])([0-9][0-9])$/; + +/** + * return date in local time formatted in iso-8601 like format + * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' + * + * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' + * @returns {String} date in local time in iso-8601 like format + * + * @globals tzRe + */ +function formatDateISOLocal(epoch, timezoneInfo) { + var match = tzRe.exec(timezoneInfo); + // date corrected by timezone + var localDate = new Date(1000 * (epoch + + (parseInt(match[1],10)*3600 + parseInt(match[2],10)*60))); + var localDateStr = // e.g. '2005-08-07' + localDate.getUTCFullYear() + '-' + + padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + + padLeft(localDate.getUTCDate(), 2, '0'); + var localTimeStr = // e.g. '21:49:46' + padLeft(localDate.getUTCHours(), 2, '0') + ':' + + padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + + padLeft(localDate.getUTCSeconds(), 2, '0'); + + return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; +} + +/* ............................................................ */ +/* unquoting/unescaping filenames */ + +/**#@+ + * @constant + */ +var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g; +var octEscRe = /^[0-7]{1,3}$/; +var maybeQuotedRe = /^\"(.*)\"$/; +/**#@-*/ + +/** + * unquote maybe git-quoted filename + * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a' + * + * @param {String} str: git-quoted string + * @returns {String} Unquoted and unescaped string + * + * @globals escCodeRe, octEscRe, maybeQuotedRe + */ +function unquote(str) { + function unq(seq) { + var es = { + // character escape codes, aka escape sequences (from C) + // replacements are to some extent JavaScript specific + t: "\t", // tab (HT, TAB) + n: "\n", // newline (NL) + r: "\r", // return (CR) + f: "\f", // form feed (FF) + b: "\b", // backspace (BS) + a: "\x07", // alarm (bell) (BEL) + e: "\x1B", // escape (ESC) + v: "\v" // vertical tab (VT) + }; + + if (seq.search(octEscRe) !== -1) { + // octal char sequence + return String.fromCharCode(parseInt(seq, 8)); + } else if (seq in es) { + // C escape sequence, aka character escape code + return es[seq]; + } + // quoted ordinary character + return seq; + } + + var match = str.match(maybeQuotedRe); + if (match) { + str = match[1]; + // perhaps str = eval('"'+str+'"'); would be enough? + str = str.replace(escCodeRe, + function (substr, p1, offset, s) { return unq(p1); }); + } + return str; +} + +/* ============================================================ */ +/* main part: parsing response */ + +/** + * Function called for each blame entry, as soon as it finishes. + * It updates page via DOM manipulation, adding sha1 info, etc. + * + * @param {Commit} commit: blamed commit + * @param {Object} group: object representing group of lines, + * which blame the same commit (blame entry) + * + * @globals blamedLines + */ +function handleLine(commit, group) { + /* + This is the structure of the HTML fragment we are working + with: + + + + 123 + # times (my ext3 doesn't). + + */ + + var resline = group.resline; + + // format date and time string only once per commit + if (!commit.info) { + /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */ + commit.info = commit.author + ', ' + + formatDateISOLocal(commit.authorTime, commit.authorTimezone); + } + + // color depends on group of lines, not only on blamed commit + var colorNo = findColorNo( + document.getElementById('l'+(resline-1)), + document.getElementById('l'+(resline+group.numlines)) + ); + + // loop over lines in commit group + for (var i = 0; i < group.numlines; i++, resline++) { + var tr = document.getElementById('l'+resline); + if (!tr) { + break; + } + /* + + + 123 + # times (my ext3 doesn't). + + */ + var td_sha1 = tr.firstChild; + var a_sha1 = td_sha1.firstChild; + var a_linenr = td_sha1.nextSibling.firstChild; + + /* */ + var tr_class = ''; + if (colorNo !== null) { + tr_class = 'color'+colorNo; + } + if (commit.boundary) { + tr_class += ' boundary'; + } + if (commit.nprevious === 0) { + tr_class += ' no-previous'; + } else if (commit.nprevious > 1) { + tr_class += ' multiple-previous'; + } + tr.className = tr_class; + + /* ? */ + if (i === 0) { + td_sha1.title = commit.info; + td_sha1.rowSpan = group.numlines; + + a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1; + if (a_sha1.firstChild) { + a_sha1.firstChild.data = commit.sha1.substr(0, 8); + } else { + a_sha1.appendChild( + document.createTextNode(commit.sha1.substr(0, 8))); + } + if (group.numlines >= 2) { + var fragment = document.createDocumentFragment(); + var br = document.createElement("br"); + var match = commit.author.match(/\b([A-Z])\B/g); + if (match) { + var text = document.createTextNode( + match.join('')); + } + if (br && text) { + var elem = fragment || td_sha1; + elem.appendChild(br); + elem.appendChild(text); + if (fragment) { + td_sha1.appendChild(fragment); + } + } + } + } else { + //tr.removeChild(td_sha1); // DOM2 Core way + tr.deleteCell(0); // DOM2 HTML way + } + + /* 123 */ + var linenr_commit = + ('previous' in commit ? commit.previous : commit.sha1); + var linenr_filename = + ('file_parent' in commit ? commit.file_parent : commit.filename); + a_linenr.href = projectUrl + 'a=blame_incremental' + + ';hb=' + linenr_commit + + ';f=' + encodeURIComponent(linenr_filename) + + '#l' + (group.srcline + i); + + blamedLines++; + + //updateProgressInfo(); + } +} + +// ---------------------------------------------------------------------- + +var inProgress = false; // are we processing response + +/**#@+ + * @constant + */ +var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/; +var infoRe = /^([a-z-]+) ?(.*)/; +var endRe = /^END ?([^ ]*) ?(.*)/; +/**@-*/ + +var curCommit = new Commit(); +var curGroup = {}; + +var pollTimer = null; + +/** + * Parse output from 'git blame --incremental [...]', received via + * XMLHttpRequest from server (blamedataUrl), and call handleLine + * (which updates page) as soon as blame entry is completed. + * + * @param {String[]} lines: new complete lines from blamedata server + * + * @globals commits, curCommit, curGroup, t_interval_server, cmds_server + * @globals sha1Re, infoRe, endRe + */ +function processBlameLines(lines) { + var match; + + for (var i = 0, len = lines.length; i < len; i++) { + + if ((match = sha1Re.exec(lines[i]))) { + var sha1 = match[1]; + var srcline = parseInt(match[2], 10); + var resline = parseInt(match[3], 10); + var numlines = parseInt(match[4], 10); + + var c = commits[sha1]; + if (!c) { + c = new Commit(sha1); + commits[sha1] = c; + } + curCommit = c; + + curGroup.srcline = srcline; + curGroup.resline = resline; + curGroup.numlines = numlines; + + } else if ((match = infoRe.exec(lines[i]))) { + var info = match[1]; + var data = match[2]; + switch (info) { + case 'filename': + curCommit.filename = unquote(data); + // 'filename' information terminates the entry + handleLine(curCommit, curGroup); + updateProgressInfo(); + break; + case 'author': + curCommit.author = data; + break; + case 'author-time': + curCommit.authorTime = parseInt(data, 10); + break; + case 'author-tz': + curCommit.authorTimezone = data; + break; + case 'previous': + curCommit.nprevious++; + // store only first 'previous' header + if (!'previous' in curCommit) { + var parts = data.split(' ', 2); + curCommit.previous = parts[0]; + curCommit.file_parent = unquote(parts[1]); + } + break; + case 'boundary': + curCommit.boundary = true; + break; + } // end switch + + } else if ((match = endRe.exec(lines[i]))) { + t_interval_server = match[1]; + cmds_server = match[2]; + + } else if (lines[i] !== '') { + // malformed line + + } // end if (match) + + } // end for (lines) +} + +/** + * Process new data and return pointer to end of processed part + * + * @param {String} unprocessed: new data (from nextReadPos) + * @param {Number} nextReadPos: end of last processed data + * @return {Number} end of processed data (new value for nextReadPos) + */ +function processData(unprocessed, nextReadPos) { + var lastLineEnd = unprocessed.lastIndexOf('\n'); + if (lastLineEnd !== -1) { + var lines = unprocessed.substring(0, lastLineEnd).split('\n'); + nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */; + + processBlameLines(lines); + } // end if + + return nextReadPos; +} + +/** + * Handle XMLHttpRequest errors + * + * @param {XMLHttpRequest} xhr: XMLHttpRequest object + * + * @globals pollTimer, commits, inProgress + */ +function handleError(xhr) { + errorInfo('Server error: ' + + xhr.status + ' - ' + (xhr.statusText || 'Error contacting server')); + + clearInterval(pollTimer); + commits = {}; // free memory + + inProgress = false; +} + +/** + * Called after XMLHttpRequest finishes (loads) + * + * @param {XMLHttpRequest} xhr: XMLHttpRequest object (unused) + * + * @globals pollTimer, commits, inProgress + */ +function responseLoaded(xhr) { + clearInterval(pollTimer); + + fixColorsAndGroups(); + writeTimeInterval(); + commits = {}; // free memory + + inProgress = false; +} + +/** + * handler for XMLHttpRequest onreadystatechange event + * @see startBlame + * + * @globals xhr, inProgress + */ +function handleResponse() { + + /* + * xhr.readyState + * + * Value Constant (W3C) Description + * ------------------------------------------------------------------- + * 0 UNSENT open() has not been called yet. + * 1 OPENED send() has not been called yet. + * 2 HEADERS_RECEIVED send() has been called, and headers + * and status are available. + * 3 LOADING Downloading; responseText holds partial data. + * 4 DONE The operation is complete. + */ + + if (xhr.readyState !== 4 && xhr.readyState !== 3) { + return; + } + + // the server returned error + if (xhr.readyState === 3 && xhr.status !== 200) { + return; + } + if (xhr.readyState === 4 && xhr.status !== 200) { + handleError(xhr); + return; + } + + // In konqueror xhr.responseText is sometimes null here... + if (xhr.responseText === null) { + return; + } + + // in case we were called before finished processing + if (inProgress) { + return; + } else { + inProgress = true; + } + + // extract new whole (complete) lines, and process them + while (xhr.prevDataLength !== xhr.responseText.length) { + if (xhr.readyState === 4 && + xhr.prevDataLength === xhr.responseText.length) { + break; + } + + xhr.prevDataLength = xhr.responseText.length; + var unprocessed = xhr.responseText.substring(xhr.nextReadPos); + xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos); + } // end while + + // did we finish work? + if (xhr.readyState === 4 && + xhr.prevDataLength === xhr.responseText.length) { + responseLoaded(xhr); + } + + inProgress = false; +} + +// ============================================================ +// ------------------------------------------------------------ + +/** + * Incrementally update line data in blame_incremental view in gitweb. + * + * @param {String} blamedataUrl: URL to server script generating blame data. + * @param {String} bUrl: partial URL to project, used to generate links. + * + * Called from 'blame_incremental' view after loading table with + * file contents, a base for blame view. + * + * @globals xhr, t0, projectUrl, div_progress_bar, totalLines, pollTimer +*/ +function startBlame(blamedataUrl, bUrl) { + + xhr = createRequestObject(); + if (!xhr) { + errorInfo('ERROR: XMLHttpRequest not supported'); + return; + } + + t0 = new Date(); + projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';'); + if ((div_progress_bar = document.getElementById('progress_bar'))) { + //div_progress_bar.setAttribute('style', 'width: 100%;'); + div_progress_bar.style.cssText = 'width: 100%;'; + } + totalLines = countLines(); + updateProgressInfo(); + + /* add extra properties to xhr object to help processing response */ + xhr.prevDataLength = -1; // used to detect if we have new data + xhr.nextReadPos = 0; // where unread part of response starts + + xhr.onreadystatechange = handleResponse; + //xhr.onreadystatechange = function () { handleResponse(xhr); }; + + xhr.open('GET', blamedataUrl); + xhr.setRequestHeader('Accept', 'text/plain'); + xhr.send(null); + + // not all browsers call onreadystatechange event on each server flush + // poll response using timer every second to handle this issue + pollTimer = setInterval(xhr.onreadystatechange, 1000); +} + +// end of gitweb.js diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 681e635090..7e477af956 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -18,6 +18,12 @@ use File::Find qw(); use File::Basename qw(basename); binmode STDOUT, ':utf8'; +our $t0; +if (eval { require Time::HiRes; 1; }) { + $t0 = [Time::HiRes::gettimeofday()]; +} +our $number_of_git_cmds = 0; + BEGIN { CGI->compile() if $ENV{'MOD_PERL'}; } @@ -90,6 +96,8 @@ our $stylesheet = undef; our $logo = "++GITWEB_LOGO++"; # URI of GIT favicon, assumed to be image/png type our $favicon = "++GITWEB_FAVICON++"; +# URI of gitweb.js (JavaScript code for gitweb) +our $javascript = "++GITWEB_JS++"; # URI and label (title) of GIT logo link #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/"; @@ -417,6 +425,20 @@ our %feature = ( 'sub' => \&feature_avatar, 'override' => 0, 'default' => ['']}, + + # Enable displaying how much time and how many git commands + # it took to generate and display page. Disabled by default. + # Project specific override is not supported. + 'timed' => { + 'override' => 0, + 'default' => [0]}, + + # Enable turning some links into links to actions which require + # JavaScript to run (like 'blame_incremental'). Not enabled by + # default. Project specific override is currently not supported. + 'javascript-actions' => { + 'override' => 0, + 'default' => [0]}, ); sub gitweb_get_feature { @@ -531,6 +553,7 @@ if (-e $GITWEB_CONFIG) { # version of the core git binary our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown"; +$number_of_git_cmds++; $projects_list ||= $projectroot; @@ -568,12 +591,16 @@ our @cgi_param_mapping = ( snapshot_format => "sf", extra_options => "opt", search_use_regexp => "sr", + # this must be last entry (for manipulation from JavaScript) + javascript => "js" ); our %cgi_param_mapping = @cgi_param_mapping; # we will also need to know the possible actions, for validation our %actions = ( "blame" => \&git_blame, + "blame_incremental" => \&git_blame_incremental, + "blame_data" => \&git_blame_data, "blobdiff" => \&git_blobdiff, "blobdiff_plain" => \&git_blobdiff_plain, "blob" => \&git_blob, @@ -2006,6 +2033,7 @@ sub get_feed_info { # returns path to the core git executable and the --git-dir parameter as list sub git_cmd { + $number_of_git_cmds++; return $GIT, '--git-dir='.$git_dir; } @@ -2020,16 +2048,27 @@ sub quote_command { # get HEAD ref of given project as hash sub git_get_head_hash { - my $project = shift; + return git_get_full_hash(shift, 'HEAD'); +} + +sub git_get_full_hash { + return git_get_hash(@_); +} + +sub git_get_short_hash { + return git_get_hash(@_, '--short=7'); +} + +sub git_get_hash { + my ($project, $hash, @options) = @_; my $o_git_dir = $git_dir; my $retval = undef; $git_dir = "$projectroot/$project"; - if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") { - my $head = <$fd>; + if (open my $fd, '-|', git_cmd(), 'rev-parse', + '--verify', '-q', @options, $hash) { + $retval = <$fd>; + chomp $retval if defined $retval; close $fd; - if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) { - $retval = $1; - } } if (defined $o_git_dir) { $git_dir = $o_git_dir; @@ -3270,10 +3309,36 @@ sub git_footer_html { } print "\n"; # class="page_footer" + if (defined $t0 && gitweb_check_feature('timed')) { + print "
\n"; + print 'This page took '. + ''. + Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]). + ' seconds '. + ' and '. + ''. + $number_of_git_cmds. + ' git commands '. + " to generate.\n"; + print "
\n"; # class="page_footer" + } + if (-f $site_footer) { insert_file($site_footer); } + print qq!\n!; + if ($action eq 'blame_incremental') { + print qq!\n!; + } elsif (gitweb_check_feature('javascript-actions')) { + print qq!\n!; + } + print "\n" . ""; } @@ -4870,7 +4935,13 @@ sub git_tag { git_footer_html(); } -sub git_blame { +sub git_blame_common { + my $format = shift || 'porcelain'; + if ($format eq 'porcelain' && $cgi->param('js')) { + $format = 'incremental'; + $action = 'blame_incremental'; # for page title etc + } + # permissions gitweb_check_feature('blame') or die_error(403, "Blame view not allowed"); @@ -4892,123 +4963,220 @@ sub git_blame { } } - # run git-blame --porcelain - open my $fd, "-|", git_cmd(), "blame", '-p', - $hash_base, '--', $file_name - or die_error(500, "Open git-blame failed"); + my $fd; + if ($format eq 'incremental') { + # get file contents (as base) + open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash + or die_error(500, "Open git-cat-file failed"); + } elsif ($format eq 'data') { + # run git-blame --incremental + open $fd, "-|", git_cmd(), "blame", "--incremental", + $hash_base, "--", $file_name + or die_error(500, "Open git-blame --incremental failed"); + } else { + # run git-blame --porcelain + open $fd, "-|", git_cmd(), "blame", '-p', + $hash_base, '--', $file_name + or die_error(500, "Open git-blame --porcelain failed"); + } + + # incremental blame data returns early + if ($format eq 'data') { + print $cgi->header( + -type=>"text/plain", -charset => "utf-8", + -status=> "200 OK"); + local $| = 1; # output autoflush + print while <$fd>; + close $fd + or print "ERROR $!\n"; + + print 'END'; + if (defined $t0 && gitweb_check_feature('timed')) { + print ' '. + Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]). + ' '.$number_of_git_cmds; + } + print "\n"; + + return; + } # page header git_header_html(); my $formats_nav = $cgi->a({-href => href(action=>"blob", -replay=>1)}, "blob") . + " | "; + if ($format eq 'incremental') { + $formats_nav .= + $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)}, + "blame") . " (non-incremental)"; + } else { + $formats_nav .= + $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)}, + "blame") . " (incremental)"; + } + $formats_nav .= " | " . $cgi->a({-href => href(action=>"history", -replay=>1)}, "history") . " | " . - $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, + $cgi->a({-href => href(action=>$action, file_name=>$file_name)}, "HEAD"); git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); git_print_header_div('commit', esc_html($co{'title'}), $hash_base); git_print_page_path($file_name, $ftype, $hash_base); # page body + if ($format eq 'incremental') { + print "\n"; + + print qq!
\n!; + } + + print qq!
\n!; + print qq!
... / ...
\n! + if ($format eq 'incremental'); + print qq!\n!. + #qq!\n!. + qq!\n!. + qq!\n!. + qq!\n!. + qq!\n!; + my @rev_color = qw(light dark); my $num_colors = scalar(@rev_color); my $current_color = 0; - my %metainfo = (); - print < -
CommitLineData
- -HTML - LINE: - while (my $line = <$fd>) { - chomp $line; - # the header: [] - # no for subsequent lines in group of lines - my ($full_rev, $orig_lineno, $lineno, $group_size) = - ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/); - if (!exists $metainfo{$full_rev}) { - $metainfo{$full_rev} = { 'nprevious' => 0 }; + if ($format eq 'incremental') { + my $color_class = $rev_color[$current_color]; + + #contents of a file + my $linenr = 0; + LINE: + while (my $line = <$fd>) { + chomp $line; + $linenr++; + + print qq!!. + qq!!. + qq!!; + print qq!\n"; + print qq!\n!; } - my $meta = $metainfo{$full_rev}; - my $data; - while ($data = <$fd>) { - chomp $data; - last if ($data =~ s/^\t//); # contents of line - if ($data =~ /^(\S+)(?: (.*))?$/) { - $meta->{$1} = $2 unless exists $meta->{$1}; + + } else { # porcelain, i.e. ordinary blame + my %metainfo = (); # saves information about commits + + # blame data + LINE: + while (my $line = <$fd>) { + chomp $line; + # the header: [] + # no for subsequent lines in group of lines + my ($full_rev, $orig_lineno, $lineno, $group_size) = + ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/); + if (!exists $metainfo{$full_rev}) { + $metainfo{$full_rev} = { 'nprevious' => 0 }; } - if ($data =~ /^previous /) { - $meta->{'nprevious'}++; - } - } - my $short_rev = substr($full_rev, 0, 8); - my $author = $meta->{'author'}; - my %date = - parse_date($meta->{'author-time'}, $meta->{'author-tz'}); - my $date = $date{'iso-tz'}; - if ($group_size) { - $current_color = ($current_color + 1) % $num_colors; - } - my $tr_class = $rev_color[$current_color]; - $tr_class .= ' boundary' if (exists $meta->{'boundary'}); - $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0); - $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1); - print "\n"; - if ($group_size) { - print "\n"; - } - # 'previous' - if (exists $meta->{'previous'} && - $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) { - $meta->{'parent'} = $1; - $meta->{'file_parent'} = unquote($2); - } - my $linenr_commit = - exists($meta->{'parent'}) ? - $meta->{'parent'} : $full_rev; - my $linenr_filename = - exists($meta->{'file_parent'}) ? - $meta->{'file_parent'} : unquote($meta->{'filename'}); - my $blamed = href(action => 'blame', - file_name => $linenr_filename, - hash_base => $linenr_commit); - print ""; - print "\n"; - print "\n"; + my $short_rev = substr($full_rev, 0, 8); + my $author = $meta->{'author'}; + my %date = + parse_date($meta->{'author-time'}, $meta->{'author-tz'}); + my $date = $date{'iso-tz'}; + if ($group_size) { + $current_color = ($current_color + 1) % $num_colors; + } + my $tr_class = $rev_color[$current_color]; + $tr_class .= ' boundary' if (exists $meta->{'boundary'}); + $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0); + $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1); + print "\n"; + if ($group_size) { + print "\n"; + } + # 'previous' + if (exists $meta->{'previous'} && + $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) { + $meta->{'parent'} = $1; + $meta->{'file_parent'} = unquote($2); + } + my $linenr_commit = + exists($meta->{'parent'}) ? + $meta->{'parent'} : $full_rev; + my $linenr_filename = + exists($meta->{'file_parent'}) ? + $meta->{'file_parent'} : unquote($meta->{'filename'}); + my $blamed = href(action => 'blame', + file_name => $linenr_filename, + hash_base => $linenr_commit); + print ""; + print "\n"; + print "\n"; + } # end while + } - print "
CommitLineData
!. + qq!$linenr! . esc_html($line) . "
1); - print ">"; - print $cgi->a({-href => href(action=>"commit", - hash=>$full_rev, - file_name=>$file_name)}, - esc_html($short_rev)); - if ($group_size >= 2) { - my @author_initials = ($author =~ /\b([[:upper:]])\B/g); - if (@author_initials) { - print "
" . - esc_html(join('', @author_initials)); - # or join('.', ...) + my $meta = $metainfo{$full_rev}; + my $data; + while ($data = <$fd>) { + chomp $data; + last if ($data =~ s/^\t//); # contents of line + if ($data =~ /^(\S+)(?: (.*))?$/) { + $meta->{$1} = $2 unless exists $meta->{$1}; + } + if ($data =~ /^previous /) { + $meta->{'nprevious'}++; } } - print "
"; - print $cgi->a({ -href => "$blamed#l$orig_lineno", - -class => "linenr" }, - esc_html($lineno)); - print "" . esc_html($data) . "
1); + print ">"; + print $cgi->a({-href => href(action=>"commit", + hash=>$full_rev, + file_name=>$file_name)}, + esc_html($short_rev)); + if ($group_size >= 2) { + my @author_initials = ($author =~ /\b([[:upper:]])\B/g); + if (@author_initials) { + print "
" . + esc_html(join('', @author_initials)); + # or join('.', ...) + } + } + print "
"; + print $cgi->a({ -href => "$blamed#l$orig_lineno", + -class => "linenr" }, + esc_html($lineno)); + print "" . esc_html($data) . "
\n"; - print "
"; + + # footer + print "\n". + "\n"; # class="blame" + print "\n"; # class="blame_body" close $fd or print "Reading blob failed\n"; - # page footer git_footer_html(); } +sub git_blame { + git_blame_common(); +} + +sub git_blame_incremental { + git_blame_common('incremental'); +} + +sub git_blame_data { + git_blame_common('data'); +} + sub git_tags { my $head = git_get_head_hash($project); git_header_html(); @@ -5285,6 +5453,43 @@ sub git_tree { git_footer_html(); } +sub snapshot_name { + my ($project, $hash) = @_; + + # path/to/project.git -> project + # path/to/project/.git -> project + my $name = to_utf8($project); + $name =~ s,([^/])/*\.git$,$1,; + $name = basename($name); + # sanitize name + $name =~ s/[[:cntrl:]]/?/g; + + my $ver = $hash; + if ($hash =~ /^[0-9a-fA-F]+$/) { + # shorten SHA-1 hash + my $full_hash = git_get_full_hash($project, $hash); + if ($full_hash =~ /^$hash/ && length($hash) > 7) { + $ver = git_get_short_hash($project, $hash); + } + } elsif ($hash =~ m!^refs/tags/(.*)$!) { + # tags don't need shortened SHA-1 hash + $ver = $1; + } else { + # branches and other need shortened SHA-1 hash + if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) { + $ver = $1; + } + $ver .= '-' . git_get_short_hash($project, $hash); + } + # in case of hierarchical branch names + $ver =~ s!/!.!g; + + # name = project-version_string + $name = "$name-$ver"; + + return wantarray ? ($name, $name) : $name; +} + sub git_snapshot { my $format = $input_params{'snapshot_format'}; if (!@snapshot_fmts) { @@ -5302,28 +5507,27 @@ sub git_snapshot { die_error(403, "Unsupported snapshot format"); } - if (!defined $hash) { - $hash = git_get_head_hash($project); + my $type = git_get_type("$hash^{}"); + if (!$type) { + die_error(404, 'Object does not exist'); + } elsif ($type eq 'blob') { + die_error(400, 'Object is not a tree-ish'); } - my $name = $project; - $name =~ s,([^/])/*\.git$,$1,; - $name = basename($name); - my $filename = to_utf8($name); - $name =~ s/\047/\047\\\047\047/g; - my $cmd; - $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}"; - $cmd = quote_command( + my ($name, $prefix) = snapshot_name($project, $hash); + my $filename = "$name$known_snapshot_formats{$format}{'suffix'}"; + my $cmd = quote_command( git_cmd(), 'archive', "--format=$known_snapshot_formats{$format}{'format'}", - "--prefix=$name/", $hash); + "--prefix=$prefix/", $hash); if (exists $known_snapshot_formats{$format}{'compressor'}) { $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}}); } + $filename =~ s/(["\\])/\\$1/g; print $cgi->header( -type => $known_snapshot_formats{$format}{'type'}, - -content_disposition => 'inline; filename="' . "$filename" . '"', + -content_disposition => 'inline; filename="' . $filename . '"', -status => '200 OK'); open my $fd, "-|", $cmd diff --git a/http-push.c b/http-push.c index 0e040f8c9a..432b20f2d9 100644 --- a/http-push.c +++ b/http-push.c @@ -408,10 +408,10 @@ static void start_put(struct transfer_request *request) curl_easy_setopt(slot->curl, CURLOPT_IOCTLDATA, &request->buffer); #endif curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null); + curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 0); curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_PUT); curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1); curl_easy_setopt(slot->curl, CURLOPT_PUT, 1); - curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 0); curl_easy_setopt(slot->curl, CURLOPT_URL, request->url); if (start_active_slot(slot)) { diff --git a/merge-recursive.c b/merge-recursive.c index baabe2c356..dd4fbd0e6b 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -3,6 +3,7 @@ * Fredrik Kuivinen. * The thieves were Alex Riesen and Johannes Schindelin, in June/July 2006 */ +#include "advice.h" #include "cache.h" #include "cache-tree.h" #include "commit.h" @@ -171,18 +172,6 @@ static int git_merge_trees(int index_only, int rc; struct tree_desc t[3]; struct unpack_trees_options opts; - static const struct unpack_trees_error_msgs msgs = { - /* would_overwrite */ - "Your local changes to '%s' would be overwritten by merge. Aborting.", - /* not_uptodate_file */ - "Your local changes to '%s' would be overwritten by merge. Aborting.", - /* not_uptodate_dir */ - "Updating '%s' would lose untracked files in it. Aborting.", - /* would_lose_untracked */ - "Untracked working tree file '%s' would be %s by merge. Aborting", - /* bind_overlap -- will not happen here */ - NULL, - }; memset(&opts, 0, sizeof(opts)); if (index_only) @@ -194,7 +183,7 @@ static int git_merge_trees(int index_only, opts.fn = threeway_merge; opts.src_index = &the_index; opts.dst_index = &the_index; - opts.msgs = msgs; + opts.msgs = get_porcelain_error_msgs(); init_tree_desc_from_tree(t+0, common); init_tree_desc_from_tree(t+1, head); @@ -1182,6 +1171,28 @@ static int process_entry(struct merge_options *o, return clean_merge; } +struct unpack_trees_error_msgs get_porcelain_error_msgs(void) +{ + struct unpack_trees_error_msgs msgs = { + /* would_overwrite */ + "Your local changes to '%s' would be overwritten by merge. Aborting.", + /* not_uptodate_file */ + "Your local changes to '%s' would be overwritten by merge. Aborting.", + /* not_uptodate_dir */ + "Updating '%s' would lose untracked files in it. Aborting.", + /* would_lose_untracked */ + "Untracked working tree file '%s' would be %s by merge. Aborting", + /* bind_overlap -- will not happen here */ + NULL, + }; + if (advice_commit_before_merge) { + msgs.would_overwrite = msgs.not_uptodate_file = + "Your local changes to '%s' would be overwritten by merge. Aborting.\n" + "Please, commit your changes or stash them before you can merge."; + } + return msgs; +} + int merge_trees(struct merge_options *o, struct tree *head, struct tree *merge, diff --git a/merge-recursive.h b/merge-recursive.h index fd138ca140..d8bc7299ee 100644 --- a/merge-recursive.h +++ b/merge-recursive.h @@ -17,6 +17,9 @@ struct merge_options { struct string_list current_directory_set; }; +/* Return a list of user-friendly error messages to be used by merge */ +struct unpack_trees_error_msgs get_porcelain_error_msgs(void); + /* merge_trees() but with recursive ancestor consolidation */ int merge_recursive(struct merge_options *o, struct commit *h1, diff --git a/pretty.c b/pretty.c index 5661cba595..8f5bd1ab7f 100644 --- a/pretty.c +++ b/pretty.c @@ -628,8 +628,8 @@ static void rewrap_message_tail(struct strbuf *sb, c->indent2 = new_indent2; } -static size_t format_commit_item(struct strbuf *sb, const char *placeholder, - void *context) +static size_t format_commit_one(struct strbuf *sb, const char *placeholder, + void *context) { struct format_commit_context *c = context; const struct commit *commit = c->commit; @@ -816,6 +816,44 @@ static size_t format_commit_item(struct strbuf *sb, const char *placeholder, return 0; /* unknown placeholder */ } +static size_t format_commit_item(struct strbuf *sb, const char *placeholder, + void *context) +{ + int consumed; + size_t orig_len; + enum { + NO_MAGIC, + ADD_LF_BEFORE_NON_EMPTY, + DEL_LF_BEFORE_EMPTY, + } magic = NO_MAGIC; + + switch (placeholder[0]) { + case '-': + magic = DEL_LF_BEFORE_EMPTY; + break; + case '+': + magic = ADD_LF_BEFORE_NON_EMPTY; + break; + default: + break; + } + if (magic != NO_MAGIC) + placeholder++; + + orig_len = sb->len; + consumed = format_commit_one(sb, placeholder, context); + if (magic == NO_MAGIC) + return consumed; + + if ((orig_len == sb->len) && magic == DEL_LF_BEFORE_EMPTY) { + while (sb->len && sb->buf[sb->len - 1] == '\n') + strbuf_setlen(sb, sb->len - 1); + } else if ((orig_len != sb->len) && magic == ADD_LF_BEFORE_NON_EMPTY) { + strbuf_insert(sb, orig_len, "\n", 1); + } + return consumed + 1; +} + void format_commit_message(const struct commit *commit, const char *format, struct strbuf *sb, const struct pretty_print_context *pretty_ctx) diff --git a/refs.c b/refs.c index 808f56bb27..3e73a0a36d 100644 --- a/refs.c +++ b/refs.c @@ -286,6 +286,7 @@ static struct ref_list *get_ref_dir(const char *base, struct ref_list *list) } struct warn_if_dangling_data { + FILE *fp; const char *refname; const char *msg_fmt; }; @@ -304,13 +305,13 @@ static int warn_if_dangling_symref(const char *refname, const unsigned char *sha if (!resolves_to || strcmp(resolves_to, d->refname)) return 0; - printf(d->msg_fmt, refname); + fprintf(d->fp, d->msg_fmt, refname); return 0; } -void warn_dangling_symref(const char *msg_fmt, const char *refname) +void warn_dangling_symref(FILE *fp, const char *msg_fmt, const char *refname) { - struct warn_if_dangling_data data = { refname, msg_fmt }; + struct warn_if_dangling_data data = { fp, refname, msg_fmt }; for_each_rawref(warn_if_dangling_symref, &data); } diff --git a/refs.h b/refs.h index 777b5b7ca6..e141991851 100644 --- a/refs.h +++ b/refs.h @@ -29,7 +29,7 @@ extern int for_each_replace_ref(each_ref_fn, void *); /* can be used to learn about broken ref and symref */ extern int for_each_rawref(each_ref_fn, void *); -extern void warn_dangling_symref(const char *msg_fmt, const char *refname); +extern void warn_dangling_symref(FILE *fp, const char *msg_fmt, const char *refname); /* * Extra refs will be listed by for_each_ref() before any actual refs diff --git a/remote-curl.c b/remote-curl.c index 4f28c222f2..a331bae6c8 100644 --- a/remote-curl.c +++ b/remote-curl.c @@ -307,7 +307,7 @@ static size_t rpc_out(void *ptr, size_t eltsize, rpc->len = avail; } - if (max < avail); + if (max < avail) avail = max; memcpy(ptr, rpc->buf + rpc->pos, avail); rpc->pos += avail; @@ -356,8 +356,8 @@ static int post_rpc(struct rpc_state *rpc) slot = get_active_slot(); slot->results = &results; - curl_easy_setopt(slot->curl, CURLOPT_POST, 1); curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 0); + curl_easy_setopt(slot->curl, CURLOPT_POST, 1); curl_easy_setopt(slot->curl, CURLOPT_URL, rpc->service_url); curl_easy_setopt(slot->curl, CURLOPT_ENCODING, ""); diff --git a/remote.c b/remote.c index e0d17bb830..b979a9642b 100644 --- a/remote.c +++ b/remote.c @@ -397,7 +397,8 @@ static int handle_config(const char *key, const char *value, void *cb) remote->mirror = git_config_bool(key, value); else if (!strcmp(subkey, ".skipdefaultupdate")) remote->skip_default_update = git_config_bool(key, value); - + else if (!strcmp(subkey, ".skipfetchall")) + remote->skip_default_update = git_config_bool(key, value); else if (!strcmp(subkey, ".url")) { const char *v; if (git_config_string(&v, key, value)) @@ -1591,3 +1592,42 @@ struct ref *guess_remote_head(const struct ref *head, return list; } + +struct stale_heads_info { + struct remote *remote; + struct string_list *ref_names; + struct ref **stale_refs_tail; +}; + +static int get_stale_heads_cb(const char *refname, + const unsigned char *sha1, int flags, void *cb_data) +{ + struct stale_heads_info *info = cb_data; + struct refspec refspec; + memset(&refspec, 0, sizeof(refspec)); + refspec.dst = (char *)refname; + if (!remote_find_tracking(info->remote, &refspec)) { + if (!((flags & REF_ISSYMREF) || + string_list_has_string(info->ref_names, refspec.src))) { + struct ref *ref = make_linked_ref(refname, &info->stale_refs_tail); + hashcpy(ref->new_sha1, sha1); + } + } + return 0; +} + +struct ref *get_stale_heads(struct remote *remote, struct ref *fetch_map) +{ + struct ref *ref, *stale_refs = NULL; + struct string_list ref_names = { NULL, 0, 0, 0 }; + struct stale_heads_info info; + info.remote = remote; + info.ref_names = &ref_names; + info.stale_refs_tail = &stale_refs; + for (ref = fetch_map; ref; ref = ref->next) + string_list_append(ref->name, &ref_names); + sort_string_list(&ref_names); + for_each_ref(get_stale_heads_cb, &info); + string_list_clear(&ref_names, 0); + return stale_refs; +} diff --git a/remote.h b/remote.h index 5db842087d..d0aba81ead 100644 --- a/remote.h +++ b/remote.h @@ -154,4 +154,7 @@ struct ref *guess_remote_head(const struct ref *head, const struct ref *refs, int all); +/* Return refs which no longer exist on remote */ +struct ref *get_stale_heads(struct remote *remote, struct ref *fetch_map); + #endif diff --git a/revision.c b/revision.c index a36c0d9bcd..a8a3c3a4bd 100644 --- a/revision.c +++ b/revision.c @@ -953,21 +953,59 @@ int handle_revision_arg(const char *arg, struct rev_info *revs, return 0; } -void read_revisions_from_stdin(struct rev_info *revs) +static void read_pathspec_from_stdin(struct rev_info *revs, struct strbuf *sb, const char ***prune_data) { - char line[1000]; + const char **prune = *prune_data; + int prune_nr; + int prune_alloc; - while (fgets(line, sizeof(line), stdin) != NULL) { - int len = strlen(line); - if (len && line[len - 1] == '\n') - line[--len] = '\0'; + /* count existing ones */ + if (!prune) + prune_nr = 0; + else + for (prune_nr = 0; prune[prune_nr]; prune_nr++) + ; + prune_alloc = prune_nr; /* not really, but we do not know */ + + while (strbuf_getwholeline(sb, stdin, '\n') != EOF) { + int len = sb->len; + if (len && sb->buf[len - 1] == '\n') + sb->buf[--len] = '\0'; + ALLOC_GROW(prune, prune_nr+1, prune_alloc); + prune[prune_nr++] = xstrdup(sb->buf); + } + if (prune) { + ALLOC_GROW(prune, prune_nr+1, prune_alloc); + prune[prune_nr] = NULL; + } + *prune_data = prune; +} + +static void read_revisions_from_stdin(struct rev_info *revs, const char ***prune) +{ + struct strbuf sb; + int seen_dashdash = 0; + + strbuf_init(&sb, 1000); + while (strbuf_getwholeline(&sb, stdin, '\n') != EOF) { + int len = sb.len; + if (len && sb.buf[len - 1] == '\n') + sb.buf[--len] = '\0'; if (!len) break; - if (line[0] == '-') + if (sb.buf[0] == '-') { + if (len == 2 && sb.buf[1] == '-') { + seen_dashdash = 1; + break; + } die("options not supported in --stdin mode"); - if (handle_revision_arg(line, revs, 0, 1)) - die("bad revision '%s'", line); + } + if (handle_revision_arg(sb.buf, revs, 0, 1)) + die("bad revision '%s'", sb.buf); } + if (seen_dashdash) + read_pathspec_from_stdin(revs, &sb, prune); + strbuf_release(&sb); } static void add_grep(struct rev_info *revs, const char *ptn, enum grep_pat_token what) @@ -1229,6 +1267,34 @@ static int for_each_good_bisect_ref(each_ref_fn fn, void *cb_data) return for_each_ref_in("refs/bisect/good", fn, cb_data); } +static void append_prune_data(const char ***prune_data, const char **av) +{ + const char **prune = *prune_data; + int prune_nr; + int prune_alloc; + + if (!prune) { + *prune_data = av; + return; + } + + /* count existing ones */ + for (prune_nr = 0; prune[prune_nr]; prune_nr++) + ; + prune_alloc = prune_nr; /* not really, but we do not know */ + + while (*av) { + ALLOC_GROW(prune, prune_nr+1, prune_alloc); + prune[prune_nr++] = *av; + av++; + } + if (prune) { + ALLOC_GROW(prune, prune_nr+1, prune_alloc); + prune[prune_nr] = NULL; + } + *prune_data = prune; +} + /* * Parse revision information, filling in the "rev_info" structure, * and removing the used arguments from the argument list. @@ -1238,7 +1304,8 @@ static int for_each_good_bisect_ref(each_ref_fn fn, void *cb_data) */ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const char *def) { - int i, flags, left, seen_dashdash; + int i, flags, left, seen_dashdash, read_from_stdin; + const char **prune_data = NULL; /* First, search for "--" */ seen_dashdash = 0; @@ -1249,13 +1316,14 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch argv[i] = NULL; argc = i; if (argv[i + 1]) - revs->prune_data = get_pathspec(revs->prefix, argv + i + 1); + prune_data = argv + i + 1; seen_dashdash = 1; break; } /* Second, deal with arguments and options */ flags = 0; + read_from_stdin = 0; for (left = i = 1; i < argc; i++) { const char *arg = argv[i]; if (*arg == '-') { @@ -1300,6 +1368,16 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch revs->no_walk = 0; continue; } + if (!strcmp(arg, "--stdin")) { + if (revs->disable_stdin) { + argv[left++] = arg; + continue; + } + if (read_from_stdin++) + die("--stdin given twice?"); + read_revisions_from_stdin(revs, &prune_data); + continue; + } opts = handle_revision_opt(revs, argc - i, argv + i, &left, argv); if (opts > 0) { @@ -1325,12 +1403,14 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch for (j = i; j < argc; j++) verify_filename(revs->prefix, argv[j]); - revs->prune_data = get_pathspec(revs->prefix, - argv + i); + append_prune_data(&prune_data, argv + i); break; } } + if (prune_data) + revs->prune_data = get_pathspec(revs->prefix, prune_data); + if (revs->def == NULL) revs->def = def; if (revs->show_merge) diff --git a/revision.h b/revision.h index 921656aaab..d368003159 100644 --- a/revision.h +++ b/revision.h @@ -84,6 +84,8 @@ struct rev_info { use_terminator:1, missing_newline:1, date_mode_explicit:1; + unsigned int disable_stdin:1; + enum date_mode date_mode; unsigned int abbrev; @@ -129,8 +131,6 @@ struct rev_info { #define REV_TREE_DIFFERENT 3 /* Mixed changes */ /* revision.c */ -void read_revisions_from_stdin(struct rev_info *revs); - typedef void (*show_early_output_fn_t)(struct rev_info *, struct commit_list *); extern volatile show_early_output_fn_t show_early_output; diff --git a/t/README b/t/README index d8f6c7de6d..4e1d7dd183 100644 --- a/t/README +++ b/t/README @@ -75,6 +75,19 @@ appropriately before running "make". As the names depend on the tests' file names, it is safe to run the tests with this option in parallel. +You can also set the GIT_TEST_INSTALLED environment variable to +the bindir of an existing git installation to test that installation. +You still need to have built this git sandbox, from which various +test-* support programs, templates, and perl libraries are used. +If your installed git is incomplete, it will silently test parts of +your built version instead. + +When using GIT_TEST_INSTALLED, you can also set GIT_TEST_EXEC_PATH to +override the location of the dashed-form subcommands (what +GIT_EXEC_PATH would be used for during normal operation). +GIT_TEST_EXEC_PATH defaults to `$GIT_TEST_INSTALLED/git --exec-path`. + + Skipping Tests -------------- diff --git a/t/gitweb-lib.sh b/t/gitweb-lib.sh index 845253274b..76d8b7b803 100644 --- a/t/gitweb-lib.sh +++ b/t/gitweb-lib.sh @@ -52,10 +52,24 @@ gitweb_run () { rm -f gitweb.log && perl -- "$SCRIPT_NAME" \ >gitweb.output 2>gitweb.log && + perl -w -e ' + open O, ">gitweb.headers"; + while (<>) { + print O; + last if (/^\r$/ || /^$/); + } + open O, ">gitweb.body"; + while (<>) { + print O; + } + close O; + ' gitweb.output && if grep '^[[]' gitweb.log >/dev/null 2>&1; then false; else true; fi # gitweb.log is left for debugging - # gitweb.output is used to parse http output + # gitweb.output is used to parse HTTP output + # gitweb.headers contains only HTTP headers + # gitweb.body contains body of message, without headers } . ./test-lib.sh diff --git a/t/t1200-tutorial.sh b/t/t1200-tutorial.sh index 6bf84755f3..238c2f1c08 100755 --- a/t/t1200-tutorial.sh +++ b/t/t1200-tutorial.sh @@ -47,7 +47,8 @@ test_expect_success 'tree' ' ' test_expect_success 'git diff-index -p HEAD' ' - tree=$(git write-tree) + test_tick && + tree=$(git write-tree) && commit=$(echo "Initial commit" | git commit-tree $tree) && git update-ref HEAD $commit && git diff-index -p HEAD > diff.output && @@ -113,12 +114,14 @@ test_expect_success 'git branch' ' test_expect_success 'git resolve now fails' ' git checkout mybranch && echo "Work, work, work" >>hello && + test_tick && git commit -m "Some work." -i hello && git checkout master && echo "Play, play, play" >>hello && echo "Lots of fun" >>example && + test_tick && git commit -m "Some fun." -i hello example && test_must_fail git merge -m "Merge work in mybranch" mybranch @@ -141,6 +144,7 @@ cat > show-branch.expect << EOF EOF test_expect_success 'git show-branch' ' + test_tick && git commit -m "Merge work in mybranch" -i hello && git show-branch --topo-order --more=1 master mybranch \ > show-branch.output && @@ -201,9 +205,9 @@ cat > show-branch4.expect << EOF * [master] Some fun. ! [mybranch] Some work. -- - + [mybranch] Some work. * [master] Some fun. -*+ [mybranch^] Initial commit + + [mybranch] Some work. +*+ [master^] Initial commit EOF test_expect_success 'git show-branch (part 4)' ' diff --git a/t/t2300-cd-to-toplevel.sh b/t/t2300-cd-to-toplevel.sh index 3b01ad2e4d..9965bc5c92 100755 --- a/t/t2300-cd-to-toplevel.sh +++ b/t/t2300-cd-to-toplevel.sh @@ -8,7 +8,7 @@ test_cd_to_toplevel () { test_expect_success $3 "$2" ' ( cd '"'$1'"' && - . git-sh-setup && + . "$(git --exec-path)"/git-sh-setup && cd_to_toplevel && [ "$(pwd -P)" = "$TOPLEVEL" ] ) diff --git a/t/t3409-rebase-preserve-merges.sh b/t/t3409-rebase-preserve-merges.sh index 297d165476..8f785e7957 100755 --- a/t/t3409-rebase-preserve-merges.sh +++ b/t/t3409-rebase-preserve-merges.sh @@ -32,14 +32,14 @@ export GIT_AUTHOR_EMAIL test_expect_success 'setup for merge-preserving rebase' \ 'echo First > A && git add A && - git-commit -m "Add A1" && + git commit -m "Add A1" && git checkout -b topic && echo Second > B && git add B && - git-commit -m "Add B1" && + git commit -m "Add B1" && git checkout -f master && echo Third >> A && - git-commit -a -m "Modify A2" && + git commit -a -m "Modify A2" && git clone ./. clone1 && cd clone1 && diff --git a/t/t4014-format-patch.sh b/t/t4014-format-patch.sh index 5689d590fd..3bc1cccf88 100755 --- a/t/t4014-format-patch.sh +++ b/t/t4014-format-patch.sh @@ -549,9 +549,12 @@ test_expect_success 'options no longer allowed for format-patch' ' test_cmp expect.check output' test_expect_success 'format-patch --numstat should produce a patch' ' - git format-patch --numstat --stdout master..side | - grep "^diff --git a/" | - wc -l | - xargs test 6 = ' + git format-patch --numstat --stdout master..side > output && + test 6 = $(grep "^diff --git a/" output | wc -l)' + +test_expect_success 'format-patch -- ' ' + git format-patch master..side -- file 2>error && + ! grep "Use .--" error +' test_done diff --git a/t/t4034-diff-words.sh b/t/t4034-diff-words.sh index 21db6e95c4..1c21276c55 100755 --- a/t/t4034-diff-words.sh +++ b/t/t4034-diff-words.sh @@ -8,6 +8,7 @@ test_expect_success setup ' git config diff.color.old red git config diff.color.new green + git config diff.color.func magenta ' @@ -16,6 +17,7 @@ decrypt_color () { -e 's/.\[1m//g' \ -e 's/.\[31m//g' \ -e 's/.\[32m//g' \ + -e 's/.\[35m//g' \ -e 's/.\[36m//g' \ -e 's/.\[m//g' } @@ -49,7 +51,7 @@ cat > expect <<\EOF +++ b/post @@ -1,3 +1,7 @@ h(4)h(4),hh[44] - + a = b + c aa = a @@ -70,7 +72,7 @@ cat > expect <<\EOF +++ b/post @@ -1 +1 @@ h(4)h(4),hh[44] -@@ -3,0 +4,4 @@ a = b + c +@@ -3,0 +4,4 @@ a = b + c aa = a @@ -90,7 +92,7 @@ cat > expect <<\EOF +++ b/post @@ -1,3 +1,7 @@ h(4),hh[44] - + a = b + c aa = a @@ -126,7 +128,7 @@ cat > expect <<\EOF +++ b/post @@ -1,3 +1,7 @@ h(4),hh[44] - + a = b + c aa = a @@ -168,7 +170,7 @@ cat > expect <<\EOF +++ b/post @@ -1,3 +1,7 @@ h(4),hh[44] - + a = b + c aa = a diff --git a/t/t4107-apply-ignore-whitespace.sh b/t/t4107-apply-ignore-whitespace.sh index 484654d6e4..b04fc8fc12 100755 --- a/t/t4107-apply-ignore-whitespace.sh +++ b/t/t4107-apply-ignore-whitespace.sh @@ -136,37 +136,37 @@ void print_int(int num) { EOF test_expect_success 'file creation' ' - git-apply patch1.patch + git apply patch1.patch ' test_expect_success 'patch2 fails (retab)' ' - test_must_fail git-apply patch2.patch + test_must_fail git apply patch2.patch ' test_expect_success 'patch2 applies with --ignore-whitespace' ' - git-apply --ignore-whitespace patch2.patch + git apply --ignore-whitespace patch2.patch ' test_expect_success 'patch2 reverse applies with --ignore-space-change' ' - git-apply -R --ignore-space-change patch2.patch + git apply -R --ignore-space-change patch2.patch ' git config apply.ignorewhitespace change test_expect_success 'patch2 applies (apply.ignorewhitespace = change)' ' - git-apply patch2.patch + git apply patch2.patch ' test_expect_success 'patch3 fails (missing string at EOL)' ' - test_must_fail git-apply patch3.patch + test_must_fail git apply patch3.patch ' test_expect_success 'patch4 fails (missing EOL at EOF)' ' - test_must_fail git-apply patch4.patch + test_must_fail git apply patch4.patch ' test_expect_success 'patch5 applies (leading whitespace)' ' - git-apply patch5.patch + git apply patch5.patch ' test_expect_success 'patches do not mangle whitespace' ' @@ -175,11 +175,11 @@ test_expect_success 'patches do not mangle whitespace' ' test_expect_success 're-create file (with --ignore-whitespace)' ' rm -f main.c && - git-apply patch1.patch + git apply patch1.patch ' test_expect_success 'patch5 fails (--no-ignore-whitespace)' ' - test_must_fail git-apply --no-ignore-whitespace patch5.patch + test_must_fail git apply --no-ignore-whitespace patch5.patch ' test_done diff --git a/t/t4128-apply-root.sh b/t/t4128-apply-root.sh index 8f6aea48d8..6cc741a634 100755 --- a/t/t4128-apply-root.sh +++ b/t/t4128-apply-root.sh @@ -57,6 +57,23 @@ test_expect_success 'apply --directory (new file)' ' test content = $(cat some/sub/dir/newfile) ' +cat > patch << EOF +diff --git a/c/newfile2 b/c/newfile2 +new file mode 100644 +index 0000000..d95f3ad +--- /dev/null ++++ b/c/newfile2 +@@ -0,0 +1 @@ ++content +EOF + +test_expect_success 'apply --directory -p (new file)' ' + git reset --hard initial && + git apply -p2 --directory=some/sub/dir/ --index patch && + test content = $(git show :some/sub/dir/newfile2) && + test content = $(cat some/sub/dir/newfile2) +' + cat > patch << EOF diff --git a/delfile b/delfile deleted file mode 100644 diff --git a/t/t4201-shortlog.sh b/t/t4201-shortlog.sh index 405b971191..dd818f6fd6 100755 --- a/t/t4201-shortlog.sh +++ b/t/t4201-shortlog.sh @@ -52,4 +52,32 @@ GIT_DIR=non-existing git shortlog -w < log > out test_expect_success 'shortlog from non-git directory' 'test_cmp expect out' +iconvfromutf8toiso88591() { + printf "%s" "$*" | iconv -f UTF-8 -t ISO-8859-1 +} + +DSCHO="Jöhännës \"Dschö\" Schindëlin" +DSCHOE="$DSCHO " +MSG1="set a1 to 2 and some non-ASCII chars: Äßø" +MSG2="set a1 to 3 and some non-ASCII chars: áæï" +cat > expect << EOF +$DSCHO (2): + $MSG1 + $MSG2 + +EOF + +test_expect_success 'shortlog encoding' ' + git reset --hard "$commit" && + git config --unset i18n.commitencoding && + echo 2 > a1 && + git commit --quiet -m "$MSG1" --author="$DSCHOE" a1 && + git config i18n.commitencoding "ISO-8859-1" && + echo 3 > a1 && + git commit --quiet -m "$(iconvfromutf8toiso88591 "$MSG2")" \ + --author="$(iconvfromutf8toiso88591 "$DSCHOE")" a1 && + git config --unset i18n.commitencoding && + git shortlog HEAD~2.. > out && +test_cmp expect out' + test_done diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh index 220b6a3413..fd166d9de3 100755 --- a/t/t5505-remote.sh +++ b/t/t5505-remote.sh @@ -365,6 +365,17 @@ test_expect_success 'update with arguments' ' ' +test_expect_success 'update --prune' ' + + (cd one && + git branch -m side2 side3) && + (cd test && + git remote update --prune && + (cd ../one && git branch -m side3 side2) + git rev-parse refs/remotes/origin/side3 && + test_must_fail git rev-parse refs/remotes/origin/side2) +' + cat > one/expect << EOF apis/master apis/side diff --git a/t/t5506-remote-groups.sh b/t/t5506-remote-groups.sh index 2a1806b0b4..b7b7ddaa40 100755 --- a/t/t5506-remote-groups.sh +++ b/t/t5506-remote-groups.sh @@ -51,7 +51,7 @@ test_expect_success 'nonexistant group produces error' ' ! repo_fetched two ' -test_expect_success 'updating group updates all members' ' +test_expect_success 'updating group updates all members (remote update)' ' mark group-all && update_repos && git config --add remotes.all one && @@ -61,7 +61,15 @@ test_expect_success 'updating group updates all members' ' repo_fetched two ' -test_expect_success 'updating group does not update non-members' ' +test_expect_success 'updating group updates all members (fetch)' ' + mark fetch-group-all && + update_repos && + git fetch all && + repo_fetched one && + repo_fetched two +' + +test_expect_success 'updating group does not update non-members (remote update)' ' mark group-some && update_repos && git config --add remotes.some one && @@ -70,6 +78,15 @@ test_expect_success 'updating group does not update non-members' ' ! repo_fetched two ' +test_expect_success 'updating group does not update non-members (fetch)' ' + mark fetch-group-some && + update_repos && + git config --add remotes.some one && + git remote update some && + repo_fetched one && + ! repo_fetched two +' + test_expect_success 'updating remote name updates that remote' ' mark remote-name && update_repos && diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh new file mode 100755 index 0000000000..b73733219d --- /dev/null +++ b/t/t5514-fetch-multiple.sh @@ -0,0 +1,154 @@ +#!/bin/sh + +test_description='fetch --all works correctly' + +. ./test-lib.sh + +setup_repository () { + mkdir "$1" && ( + cd "$1" && + git init && + >file && + git add file && + test_tick && + git commit -m "Initial" && + git checkout -b side && + >elif && + git add elif && + test_tick && + git commit -m "Second" && + git checkout master + ) +} + +test_expect_success setup ' + setup_repository one && + setup_repository two && + ( + cd two && git branch another + ) && + git clone --mirror two three + git clone one test +' + +cat > test/expect << EOF + one/master + one/side + origin/HEAD -> origin/master + origin/master + origin/side + three/another + three/master + three/side + two/another + two/master + two/side +EOF + +test_expect_success 'git fetch --all' ' + (cd test && + git remote add one ../one && + git remote add two ../two && + git remote add three ../three && + git fetch --all && + git branch -r > output && + test_cmp expect output) +' + +test_expect_success 'git fetch --all should continue if a remote has errors' ' + (git clone one test2 && + cd test2 && + git remote add bad ../non-existing && + git remote add one ../one && + git remote add two ../two && + git remote add three ../three && + test_must_fail git fetch --all && + git branch -r > output && + test_cmp ../test/expect output) +' + +test_expect_success 'git fetch --all does not allow non-option arguments' ' + (cd test && + test_must_fail git fetch --all origin && + test_must_fail git fetch --all origin master) +' + +cat > expect << EOF + origin/HEAD -> origin/master + origin/master + origin/side + three/another + three/master + three/side +EOF + +test_expect_success 'git fetch --multiple (but only one remote)' ' + (git clone one test3 && + cd test3 && + git remote add three ../three && + git fetch --multiple three && + git branch -r > output && + test_cmp ../expect output) +' + +cat > expect << EOF + one/master + one/side + two/another + two/master + two/side +EOF + +test_expect_success 'git fetch --multiple (two remotes)' ' + (git clone one test4 && + cd test4 && + git remote rm origin && + git remote add one ../one && + git remote add two ../two && + git fetch --multiple one two && + git branch -r > output && + test_cmp ../expect output) +' + +test_expect_success 'git fetch --multiple (bad remote names)' ' + (cd test4 && + test_must_fail git fetch --multiple four) +' + + +test_expect_success 'git fetch --all (skipFetchAll)' ' + (cd test4 && + for b in $(git branch -r) + do + git branch -r -d $b || break + done && + git remote add three ../three && + git config remote.three.skipFetchAll true && + git fetch --all && + git branch -r > output && + test_cmp ../expect output) +' + +cat > expect << EOF + one/master + one/side + three/another + three/master + three/side + two/another + two/master + two/side +EOF + +test_expect_success 'git fetch --multiple (ignoring skipFetchAll)' ' + (cd test4 && + for b in $(git branch -r) + do + git branch -r -d $b || break + done && + git fetch --multiple one two three && + git branch -r > output && + test_cmp ../expect output) +' + +test_done diff --git a/t/t6006-rev-list-format.sh b/t/t6006-rev-list-format.sh index 7f61ab0e52..571931588e 100755 --- a/t/t6006-rev-list-format.sh +++ b/t/t6006-rev-list-format.sh @@ -162,6 +162,28 @@ test_expect_success 'empty email' ' } ' +test_expect_success 'del LF before empty (1)' ' + git show -s --pretty=format:"%s%n%-b%nThanks%n" HEAD^^ >actual && + test $(wc -l actual && + test $(wc -l actual && + test $(wc -l actual && + test $(wc -l expect && git log -g --format="%h %gD: %gs" >actual && diff --git a/t/t6017-rev-list-stdin.sh b/t/t6017-rev-list-stdin.sh new file mode 100755 index 0000000000..f1c32dba42 --- /dev/null +++ b/t/t6017-rev-list-stdin.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# +# Copyright (c) 2009, Junio C Hamano +# + +test_description='log family learns --stdin' + +. ./test-lib.sh + +check () { + for cmd in rev-list "log --stat" + do + for i in "$@" + do + printf "%s\n" $i + done >input && + test_expect_success "check $cmd $*" ' + git $cmd $(cat input) >expect && + git $cmd --stdin actual && + sed -e "s/^/input /" input && + sed -e "s/^/output /" expect && + test_cmp expect actual + ' + done +} + +them='1 2 3 4 5 6 7' + +test_expect_success setup ' + ( + for i in 0 $them + do + for j in $them + do + echo $i.$j >file-$j && + git add file-$j || exit + done && + test_tick && + git commit -m $i || exit + done && + for i in $them + do + git checkout -b side-$i master~$i && + echo updated $i >file-$i && + git add file-$i && + test_tick && + git commit -m side-$i || exit + done + ) +' + +check master +check side-1 ^side-4 +check side-1 ^side-7 -- +check side-1 ^side-7 -- file-1 +check side-1 ^side-7 -- file-2 +check side-3 ^side-4 -- file-3 +check side-3 ^side-2 +check side-3 ^side-2 -- file-1 + +test_done diff --git a/t/t6050-replace.sh b/t/t6050-replace.sh index d4818b430a..203ffdb17a 100755 --- a/t/t6050-replace.sh +++ b/t/t6050-replace.sh @@ -77,6 +77,11 @@ test_expect_success 'test --no-replace-objects option' ' git --no-replace-objects show $HASH2 | grep "A U Thor" ' +test_expect_success 'test GIT_NO_REPLACE_OBJECTS env variable' ' + GIT_NO_REPLACE_OBJECTS=1 git cat-file commit $HASH2 | grep "author A U Thor" && + GIT_NO_REPLACE_OBJECTS=1 git show $HASH2 | grep "A U Thor" +' + cat >tag.sig <foo && + git add foo && + test_tick && + git commit -m "Initial Commit" --author Frigate\ \ && + git tag Initial && + echo "Test 1" >>foo && + test_tick && + git commit -a -C Initial && + author_header Initial >expect && + author_header HEAD >actual && + test_cmp expect actual && + + message_body Initial >expect && + message_body HEAD >actual && + test_cmp expect actual +' + +test_expect_success '-C option copies only the message with --reset-author' ' + echo "Test 2" >>foo && + test_tick && + git commit -a -C Initial --reset-author && + echo "author $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE" >expect && + author_header HEAD >actual + test_cmp expect actual && + + message_body Initial >expect && + message_body HEAD >actual && + test_cmp expect actual +' + +test_expect_success '-c option copies authorship and message' ' + echo "Test 3" >>foo && + test_tick && + EDITOR=: VISUAL=: git commit -a -c Initial && + author_header Initial >expect && + author_header HEAD >actual && + test_cmp expect actual +' + +test_expect_success '-c option copies only the message with --reset-author' ' + echo "Test 4" >>foo && + test_tick && + EDITOR=: VISUAL=: git commit -a -c Initial --reset-author && + echo "author $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE" >expect && + author_header HEAD >actual && + test_cmp expect actual && + + message_body Initial >expect && + message_body HEAD >actual && + test_cmp expect actual +' + +test_expect_success '--amend option copies authorship' ' + git checkout Initial && + echo "Test 5" >>foo && + test_tick && + git commit -a --amend -m "amend test" && + author_header Initial >expect && + author_header HEAD >actual && + + echo "amend test" >expect && + message_body HEAD >actual && + test_cmp expect actual +' + +test_expect_success '--reset-author makes the commit ours even with --amend option' ' + git checkout Initial && + echo "Test 6" >>foo && + test_tick && + git commit -a --reset-author -m "Changed again" --amend && + echo "author $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE" >expect && + author_header HEAD >actual && + test_cmp expect actual && + + echo "Changed again" >expect && + message_body HEAD >actual && + test_cmp expect actual +' + +test_expect_success '--reset-author and --author are mutually exclusive' ' + git checkout Initial && + echo "Test 7" >>foo && + test_tick && + test_must_fail git commit -a --reset-author --author="Xyzzy " +' + +test_expect_success '--reset-author should be rejected without -c/-C/--amend' ' + git checkout Initial && + echo "Test 7" >>foo && + test_tick && + test_must_fail git commit -a --reset-author -m done +' + +test_done diff --git a/t/t7604-merge-custom-message.sh b/t/t7604-merge-custom-message.sh index de977c5e2f..269cfdf267 100755 --- a/t/t7604-merge-custom-message.sh +++ b/t/t7604-merge-custom-message.sh @@ -22,15 +22,12 @@ test_expect_success 'setup' ' git tag c2 ' -cat >expected <<\EOF -custom message -Merge commit 'c2' -EOF test_expect_success 'merge c2 with a custom message' ' git reset --hard c1 && + echo >expected "custom message" && git merge -m "custom message" c2 && - git cat-file commit HEAD | sed -e "1,/^$/d" > actual && + git cat-file commit HEAD | sed -e "1,/^$/d" >actual && test_cmp expected actual ' diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh index 84a7f03d46..fb51ab3dbe 100755 --- a/t/t9001-send-email.sh +++ b/t/t9001-send-email.sh @@ -95,6 +95,40 @@ test_expect_success \ 'Verify commandline' \ 'test_cmp expected commandline1' +test_expect_success 'Send patches with --envelope-sender' ' + clean_fake_sendmail && + git send-email --envelope-sender="Patch Contributer " --suppress-cc=sob --from="Example " --to=nobody@example.com --smtp-server="$(pwd)/fake.sendmail" $patches 2>errors +' + +cat >expected <<\EOF +!patch@example.com! +!-i! +!nobody@example.com! +!author@example.com! +!one@example.com! +!two@example.com! +EOF +test_expect_success \ + 'Verify commandline' \ + 'test_cmp expected commandline1' + +test_expect_success 'Send patches with --envelope-sender=auto' ' + clean_fake_sendmail && + git send-email --envelope-sender=auto --suppress-cc=sob --from="Example " --to=nobody@example.com --smtp-server="$(pwd)/fake.sendmail" $patches 2>errors +' + +cat >expected <<\EOF +!nobody@example.com! +!-i! +!nobody@example.com! +!author@example.com! +!one@example.com! +!two@example.com! +EOF +test_expect_success \ + 'Verify commandline' \ + 'test_cmp expected commandline1' + cat >expected-show-all-headers <<\EOF 0001-Second.patch (mbox) Adding cc: A from line 'From: A ' @@ -769,4 +803,53 @@ test_expect_success 'threading but no chain-reply-to' ' grep "In-Reply-To: " stdout ' +test_expect_success 'warning with an implicit --chain-reply-to' ' + git send-email \ + --dry-run \ + --from="Example " \ + --to=nobody@example.com \ + outdir/000?-*.patch 2>errors >out && + grep "no-chain-reply-to" errors +' + +test_expect_success 'no warning with an explicit --chain-reply-to' ' + git send-email \ + --dry-run \ + --from="Example " \ + --to=nobody@example.com \ + --chain-reply-to \ + outdir/000?-*.patch 2>errors >out && + ! grep "no-chain-reply-to" errors +' + +test_expect_success 'no warning with an explicit --no-chain-reply-to' ' + git send-email \ + --dry-run \ + --from="Example " \ + --to=nobody@example.com \ + --no-chain-reply-to \ + outdir/000?-*.patch 2>errors >out && + ! grep "no-chain-reply-to" errors +' + +test_expect_success 'no warning with sendemail.chainreplyto = false' ' + git config sendemail.chainreplyto false && + git send-email \ + --dry-run \ + --from="Example " \ + --to=nobody@example.com \ + outdir/000?-*.patch 2>errors >out && + ! grep "no-chain-reply-to" errors +' + +test_expect_success 'no warning with sendemail.chainreplyto = true' ' + git config sendemail.chainreplyto true && + git send-email \ + --dry-run \ + --from="Example " \ + --to=nobody@example.com \ + outdir/000?-*.patch 2>errors >out && + ! grep "no-chain-reply-to" errors +' + test_done diff --git a/t/t9150-svk-mergetickets.sh b/t/t9150-svk-mergetickets.sh index dd0c2bad24..53581425c4 100755 --- a/t/t9150-svk-mergetickets.sh +++ b/t/t9150-svk-mergetickets.sh @@ -18,7 +18,7 @@ test_expect_success 'load svk depot' " uuid=b48289b2-9c08-4d72-af37-0358a40b9c15 test_expect_success 'svk merges were represented coming in' " - [ `git-cat-file commit HEAD | grep parent | wc -l` -eq 2 ] + [ `git cat-file commit HEAD | grep parent | wc -l` -eq 2 ] " test_done diff --git a/t/t9501-gitweb-standalone-http-status.sh b/t/t9501-gitweb-standalone-http-status.sh index d0ff21d426..0688a57e1d 100755 --- a/t/t9501-gitweb-standalone-http-status.sh +++ b/t/t9501-gitweb-standalone-http-status.sh @@ -75,4 +75,43 @@ test_expect_success \ test_debug 'cat gitweb.output' +# ---------------------------------------------------------------------- +# snapshot hash ids + +test_expect_success 'snapshots: good tree-ish id' ' + gitweb_run "p=.git;a=snapshot;h=master;sf=tgz" && + grep "Status: 200 OK" gitweb.output +' +test_debug 'cat gitweb.output' + +test_expect_success 'snapshots: bad tree-ish id' ' + gitweb_run "p=.git;a=snapshot;h=frizzumFrazzum;sf=tgz" && + grep "404 - Object does not exist" gitweb.output +' +test_debug 'cat gitweb.output' + +test_expect_success 'snapshots: bad tree-ish id (tagged object)' ' + echo object > tag-object && + git add tag-object && + git commit -m "Object to be tagged" && + git tag tagged-object `git hash-object tag-object` && + gitweb_run "p=.git;a=snapshot;h=tagged-object;sf=tgz" && + grep "400 - Object is not a tree-ish" gitweb.output +' +test_debug 'cat gitweb.output' + +test_expect_success 'snapshots: good object id' ' + ID=`git rev-parse --verify HEAD` && + gitweb_run "p=.git;a=snapshot;h=$ID;sf=tgz" && + grep "Status: 200 OK" gitweb.output +' +test_debug 'cat gitweb.output' + +test_expect_success 'snapshots: bad object id' ' + gitweb_run "p=.git;a=snapshot;h=abcdef01234;sf=tgz" && + grep "404 - Object does not exist" gitweb.output +' +test_debug 'cat gitweb.output' + + test_done diff --git a/t/t9502-gitweb-standalone-parse-output.sh b/t/t9502-gitweb-standalone-parse-output.sh new file mode 100755 index 0000000000..dd83890001 --- /dev/null +++ b/t/t9502-gitweb-standalone-parse-output.sh @@ -0,0 +1,115 @@ +#!/bin/sh +# +# Copyright (c) 2009 Mark Rada +# + +test_description='gitweb as standalone script (parsing script output). + +This test runs gitweb (git web interface) as a CGI script from the +commandline, and checks that it produces the correct output, either +in the HTTP header or the actual script output.' + + +. ./gitweb-lib.sh + +# ---------------------------------------------------------------------- +# snapshot file name and prefix + +cat >>gitweb_config.perl <<\EOF + +$known_snapshot_formats{'tar'} = { + 'display' => 'tar', + 'type' => 'application/x-tar', + 'suffix' => '.tar', + 'format' => 'tar', +}; + +$feature{'snapshot'}{'default'} = ['tar']; +EOF + +# Call check_snapshot with the arguments " []" +# +# This will check that gitweb HTTP header contains proposed filename +# as with '.tar' suffix added, and that generated tarfile +# (gitweb message body) has as prefix for al files in tarfile +# +# default to +check_snapshot () { + basename=$1 + prefix=${2:-"$1"} + echo "basename=$basename" + grep "filename=.*$basename.tar" gitweb.headers >/dev/null 2>&1 && + "$TAR" tf gitweb.body >file_list && + ! grep -v "^$prefix/" file_list +} + +test_expect_success setup ' + test_commit first foo && + git branch xx/test && + FULL_ID=$(git rev-parse --verify HEAD) && + SHORT_ID=$(git rev-parse --verify --short=7 HEAD) +' +test_debug ' + echo "FULL_ID = $FULL_ID" + echo "SHORT_ID = $SHORT_ID" +' + +test_expect_success 'snapshot: full sha1' ' + gitweb_run "p=.git;a=snapshot;h=$FULL_ID;sf=tar" && + check_snapshot ".git-$SHORT_ID" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: shortened sha1' ' + gitweb_run "p=.git;a=snapshot;h=$SHORT_ID;sf=tar" && + check_snapshot ".git-$SHORT_ID" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: almost full sha1' ' + ID=$(git rev-parse --short=30 HEAD) && + gitweb_run "p=.git;a=snapshot;h=$ID;sf=tar" && + check_snapshot ".git-$SHORT_ID" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: HEAD' ' + gitweb_run "p=.git;a=snapshot;h=HEAD;sf=tar" && + check_snapshot ".git-HEAD-$SHORT_ID" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: short branch name (master)' ' + gitweb_run "p=.git;a=snapshot;h=master;sf=tar" && + ID=$(git rev-parse --verify --short=7 master) && + check_snapshot ".git-master-$ID" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: short tag name (first)' ' + gitweb_run "p=.git;a=snapshot;h=first;sf=tar" && + ID=$(git rev-parse --verify --short=7 first) && + check_snapshot ".git-first-$ID" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: full branch name (refs/heads/master)' ' + gitweb_run "p=.git;a=snapshot;h=refs/heads/master;sf=tar" && + ID=$(git rev-parse --verify --short=7 master) && + check_snapshot ".git-master-$ID" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: full tag name (refs/tags/first)' ' + gitweb_run "p=.git;a=snapshot;h=refs/tags/first;sf=tar" && + check_snapshot ".git-first" +' +test_debug 'cat gitweb.headers && cat file_list' + +test_expect_success 'snapshot: hierarchical branch name (xx/test)' ' + gitweb_run "p=.git;a=snapshot;h=xx/test;sf=tar" && + ! grep "filename=.*/" gitweb.headers +' +test_debug 'cat gitweb.headers' + +test_done diff --git a/utf8.c b/utf8.c index 01d1869fa3..7ddff23fa7 100644 --- a/utf8.c +++ b/utf8.c @@ -314,6 +314,20 @@ static void strbuf_add_indented_text(struct strbuf *buf, const char *text, } } +static size_t display_mode_esc_sequence_len(const char *s) +{ + const char *p = s; + if (*p++ != '\033') + return 0; + if (*p++ != '[') + return 0; + while (isdigit(*p) || *p == ';') + p++; + if (*p++ != 'm') + return 0; + return p - s; +} + /* * Wrap the text, if necessary. The variable indent is the indent for the * first line, indent2 is the indent for all other lines. @@ -337,7 +351,13 @@ int strbuf_add_wrapped_text(struct strbuf *buf, } for (;;) { - char c = *text; + char c; + size_t skip; + + while ((skip = display_mode_esc_sequence_len(text))) + text += skip; + + c = *text; if (!c || isspace(c)) { if (w < width || !space) { const char *start = bol;