Merge branch 'stable-2.15'
* stable-2.15:
Skip migrating inline comments on missing patch set parents
Temporarily increase heap size of NoteDb migration tests
Log the reason why a project cannot be found
Change kind cache: short-circuit on root commits
Document that gitweb.type must be set
Tidy up config-gitweb
Change kind cache: short-circuit on root commits
Do not abort indexing if < 50% projects failed
Improve documentation of `index.maxLimit` for Elasticsearch
InitIndex: Set Elasticsearch index config under elasticsearch section
Link to hashtag intro docs from more places
user-upload.txt: Document setting hashtags on push
intro-user.txt: Document hashtags
user-search.txt: Document hashtag operator
intro-user.txt: Mention that topics may affect submission
Add NoteDb migration test for change with no patch set refs
NoteDbMigrator: Totally skip changes with no patch sets
Add more tests for rebuilding changes missing some entities
Fix Change-Id in revert email
Widen set of My Drafts menus that are automatically removed
Migrate old My Drafts menus in refs/users/default
This partially reverts commit e518d9dacc9d4cc547cb5935101859e36072ccb0
because Schema_159_to_160_Test references PREFERENCES which was made
private, and uses the forDefault method which was removed. This commit
makes PREFERENCES package visible and re-adds the forDefault method as
a package visible method.
Change-Id: Ifba662a47197b3a5f17988fc69896cdca1ff853b
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index cc03334..72e309a 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -859,8 +859,8 @@
[[category_edit_hashtags]]
=== Edit Hashtags
-This category permits users to add or remove hashtags on a change that
-is uploaded for review.
+This category permits users to add or remove
+link:intro-user.html#hashtags[hashtags] on a change that is uploaded for review.
The change owner, branch owners, project owners, and site administrators
can always edit or remove hashtags (even without having the `Edit Hashtags`
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index ba346e7..a75f610 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -151,7 +151,8 @@
=== Hashtags Changed
-Sent when the hashtags have been added to or removed from a change.
+Sent when the link:intro-user.html#hashtags[hashtags] have been added to or
+removed from a change.
type:: "hashtags-changed"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 1b6d116..f15363d 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2702,6 +2702,10 @@
limit will truncate the list (but will still set `_more_changes` on
result lists). Set to 0 for no limit.
+
+When `index.type` is set to `ELASTICSEARCH`, this value should not exceed
+the `index.max_result_window` value configured on the Elasticsearch
+server.
++
Defaults to no limit.
[[index.maxPages]]index.maxPages::
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt
index fcfd0e1..d49acfee 100644
--- a/Documentation/config-gitweb.txt
+++ b/Documentation/config-gitweb.txt
@@ -17,8 +17,9 @@
Linux distributions.
----
- git config --file $site_path/etc/gerrit.config gitweb.cgi /usr/lib/cgi-bin/gitweb.cgi
- git config --file $site_path/etc/gerrit.config --unset gitweb.url
+ git config -f $site_path/etc/gerrit.config gitweb.type gitweb
+ git config -f $site_path/etc/gerrit.config gitweb.cgi /usr/lib/cgi-bin/gitweb.cgi
+ git config -f $site_path/etc/gerrit.config --unset gitweb.url
----
Alternatively, if Gerrit is served behind reverse proxy, it can
@@ -28,8 +29,9 @@
To enable this feature, set both: `gitweb.cgi` and `gitweb.url`.
----
- git config --file $site_path/etc/gerrit.config gitweb.cgi /usr/lib/cgi-bin/gitweb.cgi
- git config --file $site_path/etc/gerrit.config gitweb.url /pretty/path/to/gitweb
+ git config -f $site_path/etc/gerrit.config gitweb.type gitweb
+ git config -f $site_path/etc/gerrit.config gitweb.cgi /usr/lib/cgi-bin/gitweb.cgi
+ git config -f $site_path/etc/gerrit.config gitweb.url /pretty/path/to/gitweb
----
After updating `'$site_path'/etc/gerrit.config`, the Gerrit server must
@@ -77,13 +79,13 @@
On Ubuntu:
----
- $ sudo apt-get install gitweb
+ sudo apt-get install gitweb
----
With Yum:
----
- $ yum install gitweb
+ yum install gitweb
----
===== Configure Gitweb
@@ -125,14 +127,14 @@
Link gitweb to `/var/www/gitweb`, check `/etc/gitweb.conf` if unsure of paths:
----
- $ sudo ln -s /usr/share/gitweb /var/www/gitweb
+ sudo ln -s /usr/share/gitweb /var/www/gitweb
----
Add the gitweb directory to the Apache configuration by creating a "gitweb"
file inside the Apache conf.d directory:
----
- $ touch /etc/apache/conf.d/gitweb
+ touch /etc/apache/conf.d/gitweb
----
Add the following to /etc/apache/conf.d/gitweb:
@@ -152,7 +154,7 @@
===== Restart the Apache Web Server
----
- $ sudo /etc/init.d/apache2 restart
+ sudo /etc/init.d/apache2 restart
----
Now you should be able to view your repository projects online:
@@ -184,7 +186,7 @@
following to check:
----
-$ perl -mCGI -mEncode -mFcntl -mFile::Find -mFile::Basename -e ""
+ perl -mCGI -mEncode -mFcntl -mFile::Find -mFile::Basename -e ""
----
You may encounter the following exception:
@@ -220,21 +222,22 @@
namespace is available.
----
-$ git config -f $site_path/etc/gerrit.config --unset gitweb.cgi
-$ git config -f $site_path/etc/gerrit.config gitweb.url https://gitweb.corporation.com
+ git config -f $site_path/etc/gerrit.config gitweb.type gitweb
+ git config -f $site_path/etc/gerrit.config --unset gitweb.cgi
+ git config -f $site_path/etc/gerrit.config gitweb.url https://gitweb.corporation.com
----
-If you're not following the traditional \{projectName\}.git project naming conventions,
+If you're not following the traditional `\{projectName\}.git` project naming conventions,
you will want to customize Gerrit to read them. Add the following:
----
-$ git config -f $site_path/etc/gerrit.config gitweb.type custom
-$ git config -f $site_path/etc/gerrit.config gitweb.project ?p=\${project}\;a=summary
-$ git config -f $site_path/etc/gerrit.config gitweb.revision ?p=\${project}\;a=commit\;h=\${commit}
-$ git config -f $site_path/etc/gerrit.config gitweb.branch ?p=\${project}\;a=shortlog\;h=\${branch}
-$ git config -f $site_path/etc/gerrit.config gitweb.roottree ?p=\${project}\;a=tree\;hb=\${commit}
-$ git config -f $site_path/etc/gerrit.config gitweb.file ?p=\${project}\;hb=\${commit}\;f=\${file}
-$ git config -f $site_path/etc/gerrit.config gitweb.filehistory ?p=\${project}\;a=history\;hb=\${branch}\;f=\${file}
+ git config -f $site_path/etc/gerrit.config gitweb.type custom
+ git config -f $site_path/etc/gerrit.config gitweb.project ?p=\${project}\;a=summary
+ git config -f $site_path/etc/gerrit.config gitweb.revision ?p=\${project}\;a=commit\;h=\${commit}
+ git config -f $site_path/etc/gerrit.config gitweb.branch ?p=\${project}\;a=shortlog\;h=\${branch}
+ git config -f $site_path/etc/gerrit.config gitweb.roottree ?p=\${project}\;a=tree\;hb=\${commit}
+ git config -f $site_path/etc/gerrit.config gitweb.file ?p=\${project}\;hb=\${commit}\;f=\${file}
+ git config -f $site_path/etc/gerrit.config gitweb.filehistory ?p=\${project}\;a=history\;hb=\${branch}\;f=\${file}
----
After updating `'$site_path'/etc/gerrit.config`, the Gerrit server must
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 29e02f0..3805c80 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -472,6 +472,10 @@
the use of the command line flag `--push-option`, aliased to `-o`,
followed by `topic=...`.
+Gerrit may be link:config-gerrit.html#change.submitWholeTopic[configured] to
+submit all changes in a topic together with a single click, even when topics
+span multiple projects.
+
.Set Topic on Push
----
$ git push origin HEAD:refs/for/master%topic=multi-master
@@ -480,6 +484,30 @@
$ git push origin HEAD:refs/heads/master -o topic=multi-master
----
+[[hashtags]]
+== Using Hashtags
+
+Hashtags are freeform strings associated with a change, like on social media
+platforms. In Gerrit, you explicitly associate hashtags with changes using a
+dedicated area of the UI; they are not parsed from commit messages or comments.
+
+Similar to topics, hashtags can be used to group related changes together, and
+to search using the link:user-search.html#hashtag[`hashtag:`] operator. Unlike
+topics, a change can have multiple hashtags, and they are only used for
+informational grouping; changes with the same hashtags are not necessarily
+submitted together.
+
+The hashtag feature is only available when running under
+link:note-db.html[NoteDb].
+
+.Set Hashtag on Push
+----
+ $ git push origin HEAD:refs/for/master%t=stable-bugfix
+
+ // this is the same as:
+ $ git push origin HEAD:refs/heads/master -o t=stable-bugfix
+----
+
[[wip]]
== Work-in-Progress Changes
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index ba42304..0f030a6 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -191,6 +191,11 @@
often combined with 'branch:' and 'project:' operators to select
all related changes in a series.
+[[hashtag]]
+hashtag:'HASHTAG'::
++
+Changes whose link:intro-user.html#hashtags[hashtag] matches 'HASHTAG' exactly.
+
[[ref]]
ref:'REF'::
+
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 9be39a6..c12a38c 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -243,12 +243,11 @@
[[topic]]
==== Topic
-To include a short tag associated with all of the changes in the
-same group, such as the local topic branch name, append it after
-the destination branch name or add it with the command line flag
-`--push-option`, aliased to `-o`. In this example the short topic
-tag 'driver/i42' will be saved on each change this push creates or
-updates:
+To include a short link:intro-user.html#topics[topic] associated with all
+of the changes in the same group, such as the local topic branch name,
+append it after the destination branch name or add it with the command line
+flag `--push-option`, aliased to `-o`. In this example the short topic name
+'driver/i42' will be saved on each change this push creates or updates:
----
git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%topic=driver/i42
@@ -257,6 +256,20 @@
git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental -o topic=driver/i42
----
+[[hashtag]]
+==== Hashtag
+
+To include a link:intro-user.html#hashtags[hashtag] associated with all of the
+changes in the same group, use the `hashtag` or `t` option:
+
+----
+ // these are all equivalent
+ git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%hashtag=stable-fix
+ git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%t=stable-fix
+ git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental -o hashtag=stable-fix
+ git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental -o t=stable-fix
+----
+
[[private]]
==== Private Changes
diff --git a/java/com/google/gerrit/pgm/init/InitIndex.java b/java/com/google/gerrit/pgm/init/InitIndex.java
index 6ad0a6b..740f4ce 100644
--- a/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -40,6 +40,7 @@
private final SitePaths site;
private final InitFlags initFlags;
private final Section gerrit;
+ private final Section.Factory sections;
@Inject
InitIndex(ConsoleUI ui, Section.Factory sections, SitePaths site, InitFlags initFlags) {
@@ -48,6 +49,7 @@
this.gerrit = sections.get("gerrit", null);
this.site = site;
this.initFlags = initFlags;
+ this.sections = sections;
}
@Override
@@ -59,10 +61,15 @@
}
if (type == IndexType.ELASTICSEARCH) {
- index.select("Transport protocol", "protocol", "http", Sets.newHashSet("http", "https"));
- index.string("Hostname", "hostname", "localhost");
- index.string("Port", "port", "9200");
- index.string("Index Name", "name", "gerrit");
+ Section elasticsearch = sections.get("elasticsearch", null);
+ elasticsearch.string("Index Prefix", "prefix", "gerrit");
+ String name = ui.readString("default", "Server Name");
+
+ Section defaultServer = sections.get("elasticsearch", name);
+ defaultServer.select(
+ "Transport protocol", "protocol", "http", Sets.newHashSet("http", "https"));
+ defaultServer.string("Hostname", "hostname", "localhost");
+ defaultServer.string("Port", "port", "9200");
}
if ((site.isNew || isEmptySite()) && type == IndexType.LUCENE) {
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 1c71c70..56b1724 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -491,25 +491,21 @@
}
public static void setCommentRevId(Comment c, PatchListCache cache, Change change, PatchSet ps)
- throws OrmException {
+ throws PatchListNotAvailableException {
checkArgument(
c.key.patchSetId == ps.getId().get(),
"cannot set RevId for patch set %s on comment %s",
ps.getId(),
c);
if (c.revId == null) {
- try {
- if (Side.fromShort(c.side) == Side.PARENT) {
- if (c.side < 0) {
- c.revId = ObjectId.toString(cache.getOldId(change, ps, -c.side));
- } else {
- c.revId = ObjectId.toString(cache.getOldId(change, ps, null));
- }
+ if (Side.fromShort(c.side) == Side.PARENT) {
+ if (c.side < 0) {
+ c.revId = ObjectId.toString(cache.getOldId(change, ps, -c.side));
} else {
- c.revId = ps.getRevision().get();
+ c.revId = ObjectId.toString(cache.getOldId(change, ps, null));
}
- } catch (PatchListNotAvailableException e) {
- throw new OrmException(e);
+ } else {
+ c.revId = ps.getRevision().get();
}
}
}
@@ -576,7 +572,11 @@
// Draft may have been created by a different real user; copy the current real user. (Only
// applies to X-Gerrit-RunAs, since modifying drafts via on_behalf_of is not allowed.)
ctx.getUser().updateRealAccountId(d::setRealAuthor);
- setCommentRevId(d, patchListCache, notes.getChange(), ps);
+ try {
+ setCommentRevId(d, patchListCache, notes.getChange(), ps);
+ } catch (PatchListNotAvailableException e) {
+ throw new OrmException(e);
+ }
}
putComments(ctx.getDb(), ctx.getUpdate(psId), PUBLISHED, drafts);
}
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index fc9ae4b..7b44d8f 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -241,8 +241,15 @@
return ChangeKind.NO_CHANGE;
}
- if ((prior.getParentCount() != 1 || next.getParentCount() != 1)
- && (!onlyFirstParentChanged(prior, next) || prior.getParentCount() == 0)) {
+ if (prior.getParentCount() == 0 || next.getParentCount() == 0) {
+ // At this point we have considered all the kinds that could be applicable to root
+ // commits; the remainder of the checks in this method all assume that both commits have
+ // at least one parent.
+ return ChangeKind.REWORK;
+ }
+
+ if ((prior.getParentCount() > 1 || next.getParentCount() > 1)
+ && !onlyFirstParentChanged(prior, next)) {
// Trivial rebases done by machine only work well on 1 parent.
return ChangeKind.REWORK;
}
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 599596f..54bf0dc 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -111,20 +111,24 @@
SortedSet<ProjectHolder> projects = new TreeSet<>();
int changeCount = 0;
Stopwatch sw = Stopwatch.createStarted();
+ int projectsFailed = 0;
for (Project.NameKey name : projectCache.all()) {
try (Repository repo = repoManager.openRepository(name)) {
long size = estimateSize(repo);
changeCount += size;
projects.add(new ProjectHolder(name, size));
} catch (IOException e) {
- log.error("Error collecting projects", e);
- return new Result(sw, false, 0, 0);
+ log.error("Error collecting project {}", name, e);
+ projectsFailed++;
+ if (projectsFailed > projects.size() / 2) {
+ log.error("Over 50% of the projects could not be collected: aborted");
+ return new Result(sw, false, 0, 0);
+ }
}
pm.update(1);
}
pm.endTask();
setTotalWork(changeCount);
-
return indexAll(index, projects);
}
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 9917261..949cd82 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -46,6 +46,7 @@
import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.update.BatchUpdate;
@@ -274,7 +275,7 @@
@Override
public boolean updateChange(ChangeContext ctx)
- throws OrmException, UnprocessableEntityException {
+ throws OrmException, UnprocessableEntityException, PatchListNotAvailableException {
patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
notes = ctx.getNotes();
if (patchSet == null) {
@@ -371,7 +372,7 @@
private Comment persistentCommentFromMailComment(
ChangeContext ctx, MailComment mailComment, PatchSet patchSetForComment)
- throws OrmException, UnprocessableEntityException {
+ throws OrmException, UnprocessableEntityException, PatchListNotAvailableException {
String fileName;
// The patch set that this comment is based on is different if this
// comment was sent in reply to a comment on a previous patch set.
diff --git a/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java b/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
index e33ece9..69cc2eb 100644
--- a/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
+++ b/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
@@ -80,6 +80,18 @@
public class PrimaryStorageMigrator {
private static final Logger log = LoggerFactory.getLogger(PrimaryStorageMigrator.class);
+ /**
+ * Exception thrown during migration if the change has no {@code noteDbState} field at the
+ * beginning of the migration.
+ */
+ public static class NoNoteDbStateException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ private NoNoteDbStateException(Change.Id id) {
+ super("change " + id + " has no note_db_state; rebuild it first");
+ }
+ }
+
private final AllUsersName allUsers;
private final ChangeNotes.Factory changeNotesFactory;
private final ChangeRebuilder rebuilder;
@@ -280,9 +292,11 @@
NoteDbChangeState state = NoteDbChangeState.parse(change);
if (state == null) {
// Could rebuild the change here, but that's more complexity, and this
- // really shouldn't happen.
- throw new OrmRuntimeException(
- "change " + id + " has no note_db_state; rebuild it first");
+ // normally shouldn't happen.
+ //
+ // Known cases where this happens are described in and handled by
+ // NoteDbMigrator#canSkipPrimaryStorageMigration.
+ throw new NoNoteDbStateException(id);
}
// If the change is already read-only, then the lease is held by another
// (likely failed) migrator thread. Fail early, as we can't take over
diff --git a/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
index 9ab06e8..92a878c 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -522,8 +522,7 @@
}
private void flushEventsToDraftUpdate(
- NoteDbUpdateManager manager, EventList<DraftCommentEvent> events, Change change)
- throws OrmException {
+ NoteDbUpdateManager manager, EventList<DraftCommentEvent> events, Change change) {
if (events.isEmpty()) {
return;
}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java b/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
index c8a649e..611f32e 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
@@ -24,9 +24,13 @@
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
class CommentEvent extends Event {
+ private static final Logger log = LoggerFactory.getLogger(CommentEvent.class);
+
public final Comment c;
private final Change change;
private final PatchSet ps;
@@ -57,10 +61,19 @@
}
@Override
- void apply(ChangeUpdate update) throws OrmException {
+ void apply(ChangeUpdate update) {
checkUpdate(update);
if (c.revId == null) {
- setCommentRevId(c, cache, change, ps);
+ try {
+ setCommentRevId(c, cache, change, ps);
+ } catch (PatchListNotAvailableException e) {
+ log.warn(
+ "Unable to determine parent commit of patch set {} ({}); omitting inline comment",
+ ps.getId(),
+ ps.getRevision(),
+ c);
+ return;
+ }
}
update.putComment(PatchLineComment.Status.PUBLISHED, c);
}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java b/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
index 914930c..3bc3a58 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
@@ -24,9 +24,13 @@
import com.google.gerrit.server.notedb.ChangeDraftUpdate;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
class DraftCommentEvent extends Event {
+ private static final Logger log = LoggerFactory.getLogger(DraftCommentEvent.class);
+
public final Comment c;
private final Change change;
private final PatchSet ps;
@@ -56,9 +60,18 @@
throw new UnsupportedOperationException();
}
- void applyDraft(ChangeDraftUpdate draftUpdate) throws OrmException {
+ void applyDraft(ChangeDraftUpdate draftUpdate) {
if (c.revId == null) {
- setCommentRevId(c, cache, change, ps);
+ try {
+ setCommentRevId(c, cache, change, ps);
+ } catch (PatchListNotAvailableException e) {
+ log.warn(
+ "Unable to determine parent commit of patch set {} ({}); omitting draft inline comment",
+ ps.getId(),
+ ps.getRevision(),
+ c);
+ return;
+ }
}
draftUpdate.putComment(c);
}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
index 8e8f232..216e95a 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
@@ -33,6 +33,7 @@
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Iterables;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Ordering;
import com.google.common.collect.SetMultimap;
@@ -66,6 +67,7 @@
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
import com.google.gerrit.server.notedb.NotesMigrationState;
import com.google.gerrit.server.notedb.PrimaryStorageMigrator;
+import com.google.gerrit.server.notedb.PrimaryStorageMigrator.NoNoteDbStateException;
import com.google.gerrit.server.notedb.RepoSequence;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -605,7 +607,19 @@
executor.submit(
() -> {
try (ManualRequestContext ctx = contextHelper.open()) {
- primaryStorageMigrator.migrateToNoteDbPrimary(id);
+ try {
+ primaryStorageMigrator.migrateToNoteDbPrimary(id);
+ } catch (NoNoteDbStateException e) {
+ if (canSkipPrimaryStorageMigration(
+ ctx.getReviewDbProvider().get(), id)) {
+ log.warn(
+ "Change {} previously failed to rebuild;"
+ + " skipping primary storage migration",
+ e);
+ } else {
+ throw e;
+ }
+ }
return true;
} catch (Exception e) {
log.error("Error migrating primary storage for " + id, e);
@@ -628,6 +642,38 @@
return disableReviewDb(prev);
}
+ /**
+ * Checks whether a change is so corrupt that it can be completely skipped by the primary storage
+ * migration step.
+ *
+ * <p>To get to the point where this method is called from {@link #setNoteDbPrimary}, it means we
+ * attempted to rebuild it, and encountered an error that was then caught in {@link
+ * #rebuildProject} and skipped. As a result, there is no {@code noteDbState} field in the change
+ * by the time we get to {@link #setNoteDbPrimary}, so {@code migrateToNoteDbPrimary} throws an
+ * exception.
+ *
+ * <p>We have to do this hacky double-checking because we don't have a way for the rebuilding
+ * phase to communicate to the primary storage migration phase that the change is skippable. It
+ * would be possible to store this info in some field in this class, but there is no guarantee
+ * that the rebuild and primary storage migration phases are run in the same JVM invocation.
+ *
+ * <p>In an ideal world, we could do this through the {@link
+ * com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage} enum, having a separate value
+ * for errors. However, that would be an invasive change touching many non-migration-related parts
+ * of the NoteDb migration code, which is too risky to attempt in the stable branch where this bug
+ * had to be fixed.
+ *
+ * <p>As of this writing, the only case where this happens is when a change has no patch sets.
+ */
+ private static boolean canSkipPrimaryStorageMigration(ReviewDb db, Change.Id id) {
+ try {
+ return Iterables.isEmpty(db.patchSets().byChange(id));
+ } catch (Exception e) {
+ log.error("Error checking if change " + id + " is corrupt, assuming yes", e);
+ return true;
+ }
+ }
+
private NotesMigrationState disableReviewDb(NotesMigrationState prev) throws IOException {
return saveState(prev, NOTE_DB, c -> setAutoMigrate(c, false));
}
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 68270e2..e3d752d 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -155,6 +155,7 @@
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
throw new IOException(e);
}
+ log.warn("Cannot find project {}", projectName.get(), e);
return null;
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index afcc8f7..be689ca 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -34,6 +34,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -108,7 +109,8 @@
@Override
public boolean updateChange(ChangeContext ctx)
- throws ResourceNotFoundException, OrmException, UnprocessableEntityException {
+ throws ResourceNotFoundException, OrmException, UnprocessableEntityException,
+ PatchListNotAvailableException {
PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
if (ps == null) {
throw new ResourceNotFoundException("patch set not found: " + psId);
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
index ee57c20..e81f9f1 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
@@ -29,6 +29,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.DraftCommentResource;
import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -87,7 +88,8 @@
}
@Override
- public boolean updateChange(ChangeContext ctx) throws ResourceNotFoundException, OrmException {
+ public boolean updateChange(ChangeContext ctx)
+ throws ResourceNotFoundException, OrmException, PatchListNotAvailableException {
Optional<Comment> maybeComment =
commentsUtil.getDraft(ctx.getDb(), ctx.getNotes(), ctx.getIdentifiedUser(), key);
if (!maybeComment.isPresent()) {
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 8e78ec0..5ea72c0 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -839,7 +839,8 @@
@Override
public boolean updateChange(ChangeContext ctx)
- throws OrmException, ResourceConflictException, UnprocessableEntityException, IOException {
+ throws OrmException, ResourceConflictException, UnprocessableEntityException, IOException,
+ PatchListNotAvailableException {
user = ctx.getIdentifiedUser();
notes = ctx.getNotes();
ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
@@ -881,7 +882,7 @@
}
private boolean insertComments(ChangeContext ctx)
- throws OrmException, UnprocessableEntityException {
+ throws OrmException, UnprocessableEntityException, PatchListNotAvailableException {
Map<String, List<CommentInput>> map = in.comments;
if (map == null) {
map = Collections.emptyMap();
@@ -941,7 +942,8 @@
return !toPublish.isEmpty();
}
- private boolean insertRobotComments(ChangeContext ctx) throws OrmException {
+ private boolean insertRobotComments(ChangeContext ctx)
+ throws OrmException, PatchListNotAvailableException {
if (in.robotComments == null) {
return false;
}
@@ -952,7 +954,8 @@
return !newRobotComments.isEmpty();
}
- private List<RobotComment> getNewRobotComments(ChangeContext ctx) throws OrmException {
+ private List<RobotComment> getNewRobotComments(ChangeContext ctx)
+ throws OrmException, PatchListNotAvailableException {
List<RobotComment> toAdd = new ArrayList<>(in.robotComments.size());
Set<CommentSetEntry> existingIds =
@@ -972,7 +975,8 @@
}
private RobotComment createRobotCommentFromInput(
- ChangeContext ctx, String path, RobotCommentInput robotCommentInput) throws OrmException {
+ ChangeContext ctx, String path, RobotCommentInput robotCommentInput)
+ throws PatchListNotAvailableException {
RobotComment robotComment =
commentsUtil.newRobotComment(
ctx,
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 3017d89..e6ede34 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -33,6 +33,7 @@
import com.google.gerrit.server.change.DraftCommentResource;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -113,7 +114,8 @@
}
@Override
- public boolean updateChange(ChangeContext ctx) throws ResourceNotFoundException, OrmException {
+ public boolean updateChange(ChangeContext ctx)
+ throws ResourceNotFoundException, OrmException, PatchListNotAvailableException {
Optional<Comment> maybeComment =
commentsUtil.getDraft(ctx.getDb(), ctx.getNotes(), ctx.getIdentifiedUser(), key);
if (!maybeComment.isPresent()) {
diff --git a/java/com/google/gerrit/server/schema/Schema_160.java b/java/com/google/gerrit/server/schema/Schema_160.java
index b65870f..c5a4422 100644
--- a/java/com/google/gerrit/server/schema/Schema_160.java
+++ b/java/com/google/gerrit/server/schema/Schema_160.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.server.git.UserConfigSections.MY;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -40,17 +41,34 @@
import org.eclipse.jgit.lib.TextProgressMonitor;
/**
- * Remove "My Drafts" menu items for all users.
+ * Remove "My Drafts" menu items for all users and server-wide default preferences.
*
* <p>Since draft changes no longer exist, these menu items are obsolete.
*
- * <p>Only matches menu items (with any name) where the URL exactly matches the <a
+ * <p>Only matches menu items (with any name) where the URL exactly matches one of the following,
+ * with or without leading {@code #}:
+ *
+ * <ul>
+ * <li>/q/is:draft
+ * <li>/q/owner:self+is:draft
+ * </ul>
+ *
+ * In particular, this includes the <a
* href="https://gerrit.googlesource.com/gerrit/+/v2.14.4/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java#144">default
- * version from 2.14 and earlier</a>. Other menus containing {@code is:draft} in other positions are
- * not affected; this is still a valid predicate that matches no changes.
+ * from version 2.14 and earlier</a>.
+ *
+ * <p>Other menus containing {@code is:draft} in other positions are not affected; this is still a
+ * valid predicate that matches no changes.
*/
public class Schema_160 extends SchemaVersion {
- @VisibleForTesting static final String DEFAULT_DRAFT_ITEM = "#/q/owner:self+is:draft";
+ @VisibleForTesting static final ImmutableList<String> DEFAULT_DRAFT_ITEMS;
+
+ static {
+ String ownerSelfIsDraft = "/q/owner:self+is:draft";
+ String isDraft = "/q/is:draft";
+ DEFAULT_DRAFT_ITEMS =
+ ImmutableList.of(ownerSelfIsDraft, '#' + ownerSelfIsDraft, isDraft, '#' + isDraft);
+ }
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
@@ -75,10 +93,9 @@
ProgressMonitor pm = new TextProgressMonitor();
pm.beginTask("Removing \"My Drafts\" menu items", ProgressMonitor.UNKNOWN);
for (Account.Id id : (Iterable<Account.Id>) Accounts.readUserRefs(repo)::iterator) {
- if (removeMyDrafts(repo, id)) {
- pm.update(1);
- }
+ removeMyDrafts(repo, RefNames.refsUsers(id), pm);
}
+ removeMyDrafts(repo, RefNames.REFS_USERS_DEFAULT, pm);
pm.endTask();
}
} catch (IOException | ConfigInvalidException e) {
@@ -86,24 +103,26 @@
}
}
- private boolean removeMyDrafts(Repository repo, Account.Id id)
+ private void removeMyDrafts(Repository repo, String ref, ProgressMonitor pm)
throws IOException, ConfigInvalidException {
MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, repo);
PersonIdent ident = serverIdent.get();
md.getCommitBuilder().setAuthor(ident);
md.getCommitBuilder().setCommitter(ident);
- Prefs prefs = new Prefs(id);
+ Prefs prefs = new Prefs(ref);
prefs.load(repo);
prefs.removeMyDrafts();
prefs.commit(md);
- return prefs.dirty();
+ if (prefs.dirty()) {
+ pm.update(1);
+ }
}
private static class Prefs extends VersionedAccountPreferences {
private boolean dirty;
- Prefs(Account.Id id) {
- super(RefNames.refsUsers(id));
+ Prefs(String ref) {
+ super(ref);
}
@Override
@@ -118,7 +137,8 @@
void removeMyDrafts() {
Config cfg = getConfig();
for (String item : cfg.getSubsections(MY)) {
- if (DEFAULT_DRAFT_ITEM.equals(cfg.getString(MY, item, KEY_URL))) {
+ String value = cfg.getString(MY, item, KEY_URL);
+ if (DEFAULT_DRAFT_ITEMS.contains(value)) {
cfg.unsetSection(MY, item);
dirty = true;
}
diff --git a/java/com/google/gerrit/server/schema/VersionedAccountPreferences.java b/java/com/google/gerrit/server/schema/VersionedAccountPreferences.java
index d57b14d..0213f3b 100644
--- a/java/com/google/gerrit/server/schema/VersionedAccountPreferences.java
+++ b/java/com/google/gerrit/server/schema/VersionedAccountPreferences.java
@@ -25,12 +25,16 @@
/** Preferences for user accounts during schema migrations. */
class VersionedAccountPreferences extends VersionedMetaData {
- private static final String PREFERENCES = "preferences.config";
+ static final String PREFERENCES = "preferences.config";
static VersionedAccountPreferences forUser(Account.Id id) {
return new VersionedAccountPreferences(RefNames.refsUsers(id));
}
+ static VersionedAccountPreferences forDefault() {
+ return new VersionedAccountPreferences(RefNames.REFS_USERS_DEFAULT);
+ }
+
private final String ref;
private Config cfg;
diff --git a/java/com/google/gerrit/testing/NoteDbChecker.java b/java/com/google/gerrit/testing/NoteDbChecker.java
index 77d581b..b5dd9e9 100644
--- a/java/com/google/gerrit/testing/NoteDbChecker.java
+++ b/java/com/google/gerrit/testing/NoteDbChecker.java
@@ -19,6 +19,8 @@
import static java.util.stream.Collectors.toList;
import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ListMultimap;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
@@ -78,14 +80,17 @@
}
public void rebuildAndCheckAllChanges() throws Exception {
- rebuildAndCheckChanges(getUnwrappedDb().changes().all().toList().stream().map(Change::getId));
+ rebuildAndCheckChanges(
+ getUnwrappedDb().changes().all().toList().stream().map(Change::getId),
+ ImmutableListMultimap.of());
}
public void rebuildAndCheckChanges(Change.Id... changeIds) throws Exception {
- rebuildAndCheckChanges(Arrays.stream(changeIds));
+ rebuildAndCheckChanges(Arrays.stream(changeIds), ImmutableListMultimap.of());
}
- private void rebuildAndCheckChanges(Stream<Change.Id> changeIds) throws Exception {
+ private void rebuildAndCheckChanges(
+ Stream<Change.Id> changeIds, ListMultimap<Change.Id, String> expectedDiffs) throws Exception {
ReviewDb db = getUnwrappedDb();
List<ChangeBundle> allExpected = readExpected(changeIds);
@@ -105,7 +110,7 @@
}
}
- checkActual(allExpected, msgs);
+ checkActual(allExpected, expectedDiffs, msgs);
} finally {
notesMigration.setReadChanges(oldRead);
notesMigration.setWriteChanges(oldWrite);
@@ -113,7 +118,14 @@
}
public void checkChanges(Change.Id... changeIds) throws Exception {
- checkActual(readExpected(Arrays.stream(changeIds)), new ArrayList<>());
+ checkActual(
+ readExpected(Arrays.stream(changeIds)), ImmutableListMultimap.of(), new ArrayList<>());
+ }
+
+ public void rebuildAndCheckChange(Change.Id changeId, String... expectedDiff) throws Exception {
+ ImmutableListMultimap.Builder<Change.Id, String> b = ImmutableListMultimap.builder();
+ b.putAll(changeId, Arrays.asList(expectedDiff));
+ rebuildAndCheckChanges(Stream.of(changeId), b.build());
}
public void assertNoChangeRef(Project.NameKey project, Change.Id changeId) throws Exception {
@@ -160,7 +172,11 @@
}
}
- private void checkActual(List<ChangeBundle> allExpected, List<String> msgs) throws Exception {
+ private void checkActual(
+ List<ChangeBundle> allExpected,
+ ListMultimap<Change.Id, String> expectedDiffs,
+ List<String> msgs)
+ throws Exception {
ReviewDb db = getUnwrappedDb();
boolean oldRead = notesMigration.readChanges();
boolean oldWrite = notesMigration.rawWriteChangesSetting();
@@ -181,9 +197,14 @@
continue;
}
List<String> diff = expected.differencesFrom(actual);
- if (!diff.isEmpty()) {
+ List<String> expectedDiff = expectedDiffs.get(c.getId());
+ if (!diff.equals(expectedDiff)) {
msgs.add("Differences between ReviewDb and NoteDb for " + c + ":");
msgs.addAll(diff);
+ if (!expectedDiff.isEmpty()) {
+ msgs.add("Expected differences:");
+ msgs.addAll(expectedDiff);
+ }
msgs.add("");
} else {
System.err.println("NoteDb conversion of change " + c.getId() + " successful");
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/BUILD b/javatests/com/google/gerrit/acceptance/server/notedb/BUILD
index 20c256f..4965402 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/BUILD
@@ -7,5 +7,8 @@
"notedb",
"server",
],
+ # TODO(dborowitz): Fix leaks in local disk tests so we can reduce heap size.
+ # http://crbug.com/gerrit/8567
+ vm_args = ["-Xmx1024m"],
deps = ["//java/com/google/gerrit/server/schema"],
)
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index c0dcc9c..d18ef34 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance.server.notedb;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
@@ -23,6 +24,7 @@
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -53,6 +55,7 @@
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.ChangeUtil;
@@ -69,6 +72,8 @@
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.testing.Util;
@@ -95,6 +100,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.eclipse.jgit.junit.TestRepository;
@@ -145,6 +151,8 @@
@Inject private PatchSetInfoFactory patchSetInfoFactory;
+ @Inject private PatchListCache patchListCache;
+
@Before
public void setUp() throws Exception {
assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.OFF);
@@ -1017,8 +1025,43 @@
db.rollback();
}
- exception.expect(NoPatchSetsException.class);
- checker.rebuildAndCheckChanges(id);
+ try {
+ checker.rebuildAndCheckChanges(id);
+ assert_().fail("expected NoPatchSetsException");
+ } catch (NoPatchSetsException e) {
+ // Expected.
+ }
+
+ Change c = db.changes().get(id);
+ assertThat(c.getNoteDbState()).isNull();
+ checker.assertNoChangeRef(project, id);
+ }
+
+ @Test
+ public void rebuildChangeWithNoEntitiesOtherThanChange() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getPatchSetId().getParentKey();
+ db.changes().beginTransaction(id);
+ try {
+ db.changeMessages().delete(db.changeMessages().byChange(id));
+ db.patchSets().delete(db.patchSets().byChange(id));
+ db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id));
+ db.patchComments().delete(db.patchComments().byChange(id));
+ db.commit();
+ } finally {
+ db.rollback();
+ }
+
+ try {
+ checker.rebuildAndCheckChanges(id);
+ assert_().fail("expected NoPatchSetsException");
+ } catch (NoPatchSetsException e) {
+ // Expected.
+ }
+
+ Change c = db.changes().get(id);
+ assertThat(c.getNoteDbState()).isNull();
+ checker.assertNoChangeRef(project, id);
}
@Test
@@ -1339,6 +1382,67 @@
assertChangeUpToDate(true, id);
}
+ @Test
+ public void missingPatchSetCommitOkForCommentsNotOnParentSide() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+
+ putDraft(user, id, 1, "draft comment", null, Side.REVISION);
+ putComment(user, id, 1, "published comment", null, Side.REVISION);
+
+ ReviewDb db = getUnwrappedDb();
+ PatchSet ps = db.patchSets().get(new PatchSet.Id(id, 1));
+ ps.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ db.patchSets().update(Collections.singleton(ps));
+
+ try {
+ patchListCache.getOldId(db.changes().get(id), ps, null);
+ assert_().fail("Expected PatchListNotAvailableException");
+ } catch (PatchListNotAvailableException e) {
+ // Expected.
+ }
+
+ checker.rebuildAndCheckChanges(id);
+ }
+
+ @Test
+ public void missingPatchSetCommitOmitsCommentsOnParentSide() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+
+ CommentInfo draftInfo = putDraft(user, id, 1, "draft comment", null, Side.PARENT);
+ putComment(user, id, 1, "published comment", null, Side.PARENT);
+ CommentInfo commentInfo =
+ gApi.changes()
+ .id(id.get())
+ .comments()
+ .values()
+ .stream()
+ .flatMap(List::stream)
+ .findFirst()
+ .get();
+
+ ReviewDb db = getUnwrappedDb();
+ PatchSet ps = db.patchSets().get(new PatchSet.Id(id, 1));
+ ps.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ db.patchSets().update(Collections.singleton(ps));
+
+ try {
+ patchListCache.getOldId(db.changes().get(id), ps, null);
+ assert_().fail("Expected PatchListNotAvailableException");
+ } catch (PatchListNotAvailableException e) {
+ // Expected.
+ }
+
+ checker.rebuildAndCheckChange(
+ id,
+ Stream.of(draftInfo.id, commentInfo.id)
+ .sorted()
+ .map(c -> id + ",1," + PushOneCommit.FILE_NAME + "," + c)
+ .collect(
+ joining(", ", "PatchLineComment.Key sets differ: [", "] only in A; [] only in B")));
+ }
+
private void assertChangesReadOnly(RestApiException e) throws Exception {
Throwable cause = e.getCause();
assertThat(cause).isInstanceOf(UpdateException.class);
@@ -1388,16 +1492,24 @@
}
}
- private void putDraft(TestAccount account, Change.Id id, int line, String msg, Boolean unresolved)
+ private CommentInfo putDraft(
+ TestAccount account, Change.Id id, int line, String msg, Boolean unresolved)
+ throws Exception {
+ return putDraft(account, id, line, msg, unresolved, Side.REVISION);
+ }
+
+ private CommentInfo putDraft(
+ TestAccount account, Change.Id id, int line, String msg, Boolean unresolved, Side side)
throws Exception {
DraftInput in = new DraftInput();
+ in.side = side;
in.line = line;
in.message = msg;
in.path = PushOneCommit.FILE_NAME;
in.unresolved = unresolved;
AcceptanceTestRequestScope.Context old = setApiUser(account);
try {
- gApi.changes().id(id.get()).current().createDraft(in);
+ return gApi.changes().id(id.get()).current().createDraft(in).get();
} finally {
atrScope.set(old);
}
@@ -1405,7 +1517,14 @@
private void putComment(TestAccount account, Change.Id id, int line, String msg, String inReplyTo)
throws Exception {
+ putComment(account, id, line, msg, inReplyTo, Side.REVISION);
+ }
+
+ private void putComment(
+ TestAccount account, Change.Id id, int line, String msg, String inReplyTo, Side side)
+ throws Exception {
CommentInput in = new CommentInput();
+ in.side = side;
in.line = line;
in.message = msg;
in.inReplyTo = inReplyTo;
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
index d4990af..27dc951 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.NOTE_DB_PRIMARY_STATE;
import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB;
import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_NO_SEQUENCE;
import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY;
@@ -43,11 +44,15 @@
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.notedb.ChangeBundle;
+import com.google.gerrit.server.notedb.ChangeBundleReader;
import com.google.gerrit.server.notedb.NoteDbChangeState;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.NoteDbChangeState.RefState;
@@ -76,6 +81,7 @@
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
@@ -101,10 +107,12 @@
// migration state may result in various kinds of wrappers showing up unexpectedly.
@Inject @ReviewDbFactory private SchemaFactory<ReviewDb> schemaFactory;
- @Inject private SitePaths sitePaths;
+ @Inject private ChangeBundleReader changeBundleReader;
+ @Inject private CommentsUtil commentsUtil;
+ @Inject private DynamicSet<NotesMigrationStateListener> listeners;
@Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
@Inject private Sequences sequences;
- @Inject private DynamicSet<NotesMigrationStateListener> listeners;
+ @Inject private SitePaths sitePaths;
private FileBasedConfig noteDbConfig;
private List<RegistrationHandle> addedListeners;
@@ -433,6 +441,67 @@
}
@Test
+ public void fullMigrationOneChangeWithNoPatchSets() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange();
+ Change.Id id1 = r1.getChange().getId();
+ Change.Id id2 = r2.getChange().getId();
+
+ db.changes().beginTransaction(id2);
+ try {
+ db.patchSets().delete(db.patchSets().byChange(id2));
+ db.commit();
+ } finally {
+ db.rollback();
+ }
+
+ migrate(b -> b);
+ assertNotesMigrationState(NOTE_DB, false, false);
+
+ try (ReviewDb db = schemaFactory.open();
+ Repository repo = repoManager.openRepository(project)) {
+ assertThat(repo.exactRef(RefNames.changeMetaRef(id1))).isNotNull();
+ assertThat(db.changes().get(id1).getNoteDbState()).isEqualTo(NOTE_DB_PRIMARY_STATE);
+
+ // A change with no patch sets is so corrupt that it is completely skipped by the migration
+ // process.
+ assertThat(repo.exactRef(RefNames.changeMetaRef(id2))).isNull();
+ assertThat(db.changes().get(id2).getNoteDbState()).isNull();
+ }
+ }
+
+ @Test
+ public void fullMigrationMissingPatchSetRefs() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+
+ try (Repository repo = repoManager.openRepository(project)) {
+ RefUpdate u = repo.updateRef(new PatchSet.Id(id, 1).toRefName());
+ u.setForceUpdate(true);
+ assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+
+ ChangeBundle reviewDbBundle;
+ try (ReviewDb db = schemaFactory.open()) {
+ reviewDbBundle = changeBundleReader.fromReviewDb(db, id);
+ }
+
+ migrate(b -> b);
+ assertNotesMigrationState(NOTE_DB, false, false);
+
+ try (ReviewDb db = schemaFactory.open();
+ Repository repo = repoManager.openRepository(project)) {
+ // Change migrated successfully even though it was missing patch set refs.
+ assertThat(repo.exactRef(RefNames.changeMetaRef(id))).isNotNull();
+ assertThat(db.changes().get(id).getNoteDbState()).isEqualTo(NOTE_DB_PRIMARY_STATE);
+
+ ChangeBundle noteDbBundle =
+ ChangeBundle.fromNotes(commentsUtil, notesFactory.createChecked(db, project, id));
+ assertThat(noteDbBundle.differencesFrom(reviewDbBundle)).isEmpty();
+ }
+ }
+
+ @Test
public void autoMigrationConfig() throws Exception {
createChange();
diff --git a/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java b/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
index 8dc4244..a01d611 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
@@ -14,14 +14,19 @@
package com.google.gerrit.server.schema;
+import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS_DEFAULT;
import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
import static com.google.gerrit.server.git.UserConfigSections.MY;
-import static com.google.gerrit.server.schema.Schema_160.DEFAULT_DRAFT_ITEM;
+import static com.google.gerrit.server.schema.Schema_160.DEFAULT_DRAFT_ITEMS;
+import static com.google.gerrit.server.schema.VersionedAccountPreferences.PREFERENCES;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.MenuItem;
@@ -38,8 +43,13 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.BlobBasedConfig;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.junit.Before;
import org.junit.Rule;
@@ -67,8 +77,12 @@
@Test
public void skipUnmodified() throws Exception {
ObjectId oldMetaId = metaRef(accountId);
- assertThat(myMenusFromNoteDb(accountId).values()).doesNotContain(DEFAULT_DRAFT_ITEM);
- assertThat(myMenusFromApi(accountId).values()).doesNotContain(DEFAULT_DRAFT_ITEM);
+ ImmutableSet<String> fromNoteDb = myMenusFromNoteDb(accountId);
+ ImmutableSet<String> fromApi = myMenusFromApi(accountId);
+ for (String item : DEFAULT_DRAFT_ITEMS) {
+ assertThat(fromNoteDb).doesNotContain(item);
+ assertThat(fromApi).doesNotContain(item);
+ }
schema160.migrateData(db, new TestUpdateUI());
@@ -78,22 +92,25 @@
@Test
public void deleteItems() throws Exception {
ObjectId oldMetaId = metaRef(accountId);
- List<String> defaultNames = ImmutableList.copyOf(myMenusFromApi(accountId).keySet());
+ ImmutableSet<String> defaultNames = myMenusFromApi(accountId);
GeneralPreferencesInfo prefs = gApi.accounts().id(accountId.get()).getPreferences();
- prefs.my.add(0, new MenuItem("Something else", DEFAULT_DRAFT_ITEM + "+is:mergeable"));
- prefs.my.add(new MenuItem("Drafts", DEFAULT_DRAFT_ITEM));
- prefs.my.add(new MenuItem("Totally not drafts", DEFAULT_DRAFT_ITEM));
+ prefs.my.add(0, new MenuItem("Something else", DEFAULT_DRAFT_ITEMS.get(0) + "+is:mergeable"));
+ for (int i = 0; i < DEFAULT_DRAFT_ITEMS.size(); i++) {
+ prefs.my.add(new MenuItem("Draft entry " + i, DEFAULT_DRAFT_ITEMS.get(i)));
+ }
gApi.accounts().id(accountId.get()).setPreferences(prefs);
List<String> oldNames =
ImmutableList.<String>builder()
.add("Something else")
.addAll(defaultNames)
- .add("Drafts")
- .add("Totally not drafts")
+ .add("Draft entry 0")
+ .add("Draft entry 1")
+ .add("Draft entry 2")
+ .add("Draft entry 3")
.build();
- assertThat(myMenusFromApi(accountId).keySet()).containsExactlyElementsIn(oldNames).inOrder();
+ assertThat(myMenusFromApi(accountId)).containsExactlyElementsIn(oldNames).inOrder();
schema160.migrateData(db, new TestUpdateUI());
accountCache.evict(accountId);
@@ -104,14 +121,74 @@
List<String> newNames =
ImmutableList.<String>builder().add("Something else").addAll(defaultNames).build();
- assertThat(myMenusFromNoteDb(accountId).keySet()).containsExactlyElementsIn(newNames).inOrder();
- assertThat(myMenusFromApi(accountId).keySet()).containsExactlyElementsIn(newNames).inOrder();
+ assertThat(myMenusFromNoteDb(accountId)).containsExactlyElementsIn(newNames).inOrder();
+ assertThat(myMenusFromApi(accountId)).containsExactlyElementsIn(newNames).inOrder();
+ }
+
+ @Test
+ public void skipNonExistentRefsUsersDefault() throws Exception {
+ assertThat(readRef(REFS_USERS_DEFAULT)).isEmpty();
+ schema160.migrateData(db, new TestUpdateUI());
+ assertThat(readRef(REFS_USERS_DEFAULT)).isEmpty();
+ }
+
+ @Test
+ public void deleteDefaultItem() throws Exception {
+ assertThat(readRef(REFS_USERS_DEFAULT)).isEmpty();
+ ImmutableSet<String> defaultNames = defaultMenusFromApi();
+
+ // Setting *any* preference causes preferences.config to contain the full set of "my" sections.
+ // This mimics real-world behavior prior to the 2.15 upgrade; see Issue 8439 for details.
+ GeneralPreferencesInfo prefs = gApi.config().server().getDefaultPreferences();
+ prefs.signedOffBy = !firstNonNull(prefs.signedOffBy, false);
+ gApi.config().server().setDefaultPreferences(prefs);
+
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ Config cfg = new BlobBasedConfig(null, repo, readRef(REFS_USERS_DEFAULT).get(), PREFERENCES);
+ assertThat(cfg.getSubsections("my")).containsExactlyElementsIn(defaultNames).inOrder();
+
+ // Add more defaults directly in git, the SetPreferences endpoint doesn't respect the "my"
+ // field in the input in 2.15 and earlier.
+ cfg.setString("my", "Drafts", "url", "#/q/owner:self+is:draft");
+ cfg.setString("my", "Something else", "url", "#/q/owner:self+is:draft+is:mergeable");
+ cfg.setString("my", "Totally not drafts", "url", "#/q/owner:self+is:draft");
+ new TestRepository<>(repo)
+ .branch(REFS_USERS_DEFAULT)
+ .commit()
+ .add(PREFERENCES, cfg.toText())
+ .create();
+ }
+
+ List<String> oldNames =
+ ImmutableList.<String>builder()
+ .addAll(defaultNames)
+ .add("Drafts")
+ .add("Something else")
+ .add("Totally not drafts")
+ .build();
+ assertThat(defaultMenusFromApi()).containsExactlyElementsIn(oldNames).inOrder();
+
+ schema160.migrateData(db, new TestUpdateUI());
+
+ assertThat(readRef(REFS_USERS_DEFAULT)).isPresent();
+
+ List<String> newNames =
+ ImmutableList.<String>builder().addAll(defaultNames).add("Something else").build();
+ assertThat(myMenusFromNoteDb(VersionedAccountPreferences::forDefault).keySet())
+ .containsExactlyElementsIn(newNames)
+ .inOrder();
+ assertThat(defaultMenusFromApi()).containsExactlyElementsIn(newNames).inOrder();
+ }
+
+ private ImmutableSet<String> myMenusFromNoteDb(Account.Id id) throws Exception {
+ return myMenusFromNoteDb(() -> VersionedAccountPreferences.forUser(id)).keySet();
}
// Raw config values, bypassing the defaults set by PreferencesConfig.
- private ImmutableMap<String, String> myMenusFromNoteDb(Account.Id id) throws Exception {
+ private ImmutableMap<String, String> myMenusFromNoteDb(
+ Supplier<VersionedAccountPreferences> prefsSupplier) throws Exception {
try (Repository repo = repoManager.openRepository(allUsersName)) {
- VersionedAccountPreferences prefs = VersionedAccountPreferences.forUser(id);
+ VersionedAccountPreferences prefs = prefsSupplier.get();
prefs.load(repo);
Config cfg = prefs.getConfig();
return cfg.getSubsections(MY)
@@ -120,18 +197,27 @@
}
}
- private ImmutableMap<String, String> myMenusFromApi(Account.Id id) throws Exception {
- return gApi.accounts()
- .id(id.get())
- .getPreferences()
- .my
- .stream()
- .collect(toImmutableMap(i -> i.name, i -> i.url));
+ private ImmutableSet<String> myMenusFromApi(Account.Id id) throws Exception {
+ return myMenus(gApi.accounts().id(id.get()).getPreferences()).keySet();
+ }
+
+ private ImmutableSet<String> defaultMenusFromApi() throws Exception {
+ return myMenus(gApi.config().server().getDefaultPreferences()).keySet();
+ }
+
+ private static ImmutableMap<String, String> myMenus(GeneralPreferencesInfo prefs) {
+
+ return prefs.my.stream().collect(toImmutableMap(i -> i.name, i -> i.url));
}
private ObjectId metaRef(Account.Id id) throws Exception {
+ return readRef(RefNames.refsUsers(id))
+ .orElseThrow(() -> new AssertionError("missing ref for account " + id));
+ }
+
+ private Optional<ObjectId> readRef(String ref) throws Exception {
try (Repository repo = repoManager.openRepository(allUsersName)) {
- return repo.exactRef(RefNames.refsUsers(id)).getObjectId();
+ return Optional.ofNullable(repo.exactRef(ref)).map(Ref::getObjectId);
}
}
}